Skip to content

Pwnable tw calc

复制本地路径 | 在线编辑

需要掌握的知识点

  • 基础 ROP: Ret2syscall && Ret2libc

代码分析

main 方法

int __cdecl main(int argc, const char **argv, const char **envp)
{
    int v3; // ebx
    int v4; // ecx

    ssignal(14, (int)timeout);
    alarm(v4, v3);
    puts("=== Welcome to SECPROG calculator ===");
    fflush(stdout);
    calc();
    return puts("Merry Christmas!");
}

alarm 函数没有好说的,重点就是 calc() 函数

calc && get_expr && parse_expr

代码就不贴了,说一下代码实现功能,输出表达式字符串到 expr 数组,处理 expr 数组,处理方法相当于是栈章节学习的处理表达式方法,就是用一个数组模拟栈,把那些临时数字放入数组里面。然后数组的第一位固定表示是栈的长度,这样就可以知道栈里面的数据 push 到哪个位置。

漏洞分析

Stage 1

想了很久,但是没有想到有哪些可利用的地方,后来看了别人的题解,感觉这应该是一种经验吧。

可以看到他的实现方式是实现这样的结构:数组首位为长度,后面为内容。这样就有破绽,如果能够修改首位,可能会出问题。
这道题的破绽在下面语句。

_DWORD *__cdecl eval(_DWORD *num_stack, char op)
{
    _DWORD *result; // eax
    if ( op == '+' )
        num_stack[*num_stack - 1] += num_stack[*num_stack];
    result = num_stack;
    --*num_stack;
    return result;
}

这里很关键的是没有检查刚开始的情况, 如果输入 '+20' 这种语句, 会出错的.

刚开始 num_stack[0] == 1,那就相当于改掉了 num_stack[0] 的数据,此时 num_stack[0] 就失去对真实长度的准确性。

例子如下。

说明
1. 每次往num_stack添加数据,num_stack[0]会加一
2. 每次往num_stack添加数据,添加位置是num_stack[*num_stack]
3. 假设num_stack[360] == 100

表达式: +360+10

Round 1
num_stack[0] = 1, num_stack[1] = 360, op = '+'
num_stack[0] += num_stack[1] ---> num_stack[0] = 361
--*num_stack ---> num_stack[0] = 360

Round 2
num_stack[0] = 361, num_stack[361] = 10, op = '+'
num_stack[360] += num_stack[361] ---> num_stack[360] = 110
--*num_stack ---> num_stack[0] = 360

最终结果: 会改掉num_stack[360]内的数据

所以我们可以修改任意位置的数据!并且修改为任何数据。比如想改 num_stack[360],那么首先输入+360,获取数据,然后比较我们想改的值,假设差值为100,那么就输入+360+100,最终成功。

Stage 2

确认现在的突破口:我们即可以获取任意地址的数据,也可以修改任意地址的数据!

第一想法,在某一段写shellcode,然后要么返回地址转到该处,要么finarray某一项改为该处。

在 GDB 中查看信息,利用 vmmap,很显然没有哪一段是即有w,又有x,因此该想法失败。

0x08048000 0x080eb000 0x00000000 r-x /home/allen/Work/Pwn/pwnable004/calc
0x080eb000 0x080ed000 0x000a2000 rw- /home/allen/Work/Pwn/pwnable004/calc
0x080ed000 0x08111000 0x00000000 rw- [heap]
0xf7ffa000 0xf7ffd000 0x00000000 r-- [vvar]
0xf7ffd000 0xf7ffe000 0x00000000 r-x [vdso]
0xfffdd000 0xffffe000 0x00000000 rw- [stack]

Stage 3

既然不能采用这样操作,又因为不是动态链接,所以不能用 Ret2libc,那么就只能采用 Ret2syscall 的方法。构造栈结构如下。
test

Stage 4

现在还差一个 /bin/sh 的地址, 这个也很容易想到:通过输出 ebp 指向的内存内容, 该内容为 old ebp, 它和我们要找的地址偏移量是固定的, 所以可以调试得到结果.

坑点

Python 的有符号和无符号处理

假设地址0xFFFFFFFFF,按有符号数来说是-1,按无符号数来说是4294967295

在Python中,假设A = -1,如果(A & 0xFFFFFFFFF),最后输出结果是4294967295

虽然这样是对的,但是对面下面的语句

if old_value < stack[i]:
    p.sendline('+' + str(address) + '+' + str(stack[i] - old_value))
else:
    p.sendline('+' + str(address) + '-' + str(old_value - stack[i]))

假设old_value = 1

如果stack[i] == -1,那最后输出+(1-(-1)) = +2,最终结果为 1+2=3
如果stack[i] == 4294967295,那最后输出+(4294967295-1) = 4294967294,最终结果为 1+4294967294=4294967295

所以最后结果处理方式应该如下

if stack[i] < 0:
    stack[i] = (1<<32) - stack[i]

总结反思

  • 经验
  • 一种看似巧妙但是有破绽的结构: 数组首位表示长度,后面为数据内容
  • 上述结构破解方式:想方法用arr[0] + arr[x],即修改掉arr[0]的数据

  • 知识

  • 虽然有符号和无符号的二进制都是一样的,但是有些情况下会有所影响(见坑点)
  • 构造ROP 以及 ROPgadget 使用

Comments