• 虚拟机逆向初探(附idc脚本的简单应用)


    虚拟机逆向初探(附idc脚本的简单应用)

    1.0 虚拟机保护原理

    内容转自https://zhuanlan.zhihu.com/p/38028963

    1.1 基本原理

    这里的虚拟机当然并不是指VMWare或者VirtualBox之类的虚拟机,而是指的意思是一种解释执行系统或者模拟器(Emulator)。所以虚拟机保护技术,是将程序可执行代码转化为自定义的中间操作码(Operation_Code,如果操作码是一个字节,一般可以称为Bytecode),用以保护源程序不被逆向和篡改,opcode通过emulator解释执行,实现程序原来的功能。在这种情况下,如果要逆向程序,就需要对整个emulator结构进行逆向,理解程序功能,还需要结合opcode进行分析,整个程序逆向工程将会十分繁琐。这是一个一般虚拟机结构:

    这种虚拟化的思想,广泛用于计算机科学其他领域。从某种程度上来说,解释执行的脚本语言都需要有一个虚拟机,例如LuaVM,Python_Interpreter。静态语言类似Java通过JVM实现了平台无关性,每个安装JVM的机器都可以执行Java程序。这些虚拟机可以提供一种平台无关的编程环境,将源程序翻译为平台无关的中间码,然后翻译执行,这是JVM虚拟机的基本架构(图片来自geeks_forgeeks):

    从这个解释中,我们可以看到虚拟机保护有其缺点,就是程序运行速度会受到影响。在商用的一些虚拟机保护软件中,可以提供SDK,在编程的时候可以直接添加标记,只保护关键算法。

    1.2 分析方法

    在目前的CTF比赛中,虚拟机题目常常有两种考法:

    • 给可执行程序和opcode,逆向emulator,结合opcode文件,推出flag

    • 只给可执行程序,逆向emulator,构造opcode,读取flag

    拿到一个虚拟机之后,一般有以下几个逆向过程:

    • 分析虚拟机入口,搞清虚拟机的输入,或者opcode位置

    • 理清虚拟机结构,包括Dispatcher和各个Handler

    • 逆向各个Handler,分析opcode的意义

    调试过程中,在汇编层面调试当然是最基本最直接的方法,但是由于虚拟机Handler可能比较多,调试十分繁琐。
    若虚拟机内部没有很复杂的代码混淆,可以考虑使用IDA进行源码级调试,这对于快速整理emulator意义很有帮助。
    再进一步,可以结合IDA反编译伪代码,加上一些宏定义,加入输出,重新编译,可以十分快速的逆向整个emulator执行过程。

    2.0 入门例题:WxyVM1

    一道不是很VM的VM题,比较简单,反编译得到源代码:

    __int64 __fastcall main(int a1, char **a2, char **a3)
    {
      char v4; // [rsp+Bh] [rbp-5h]
      int i; // [rsp+Ch] [rbp-4h]
    
      puts("[WxyVM 0.0.1]");
      puts("input your flag:");
      scanf("%s", &byte_604B80);
      v4 = 1;
      sub_4005B6();
      if ( strlen(&byte_604B80) != 24 )
        v4 = 0;
      for ( i = 0; i <= 23; ++i )
      {
        if ( *(&byte_604B80 + i) != dword_601060[i] )
          v4 = 0;
      }
      if ( v4 )
        puts("correct");
      else
        puts("wrong");
      return 0LL;
    }
    

    main函数逻辑清晰,输入flag经过sub_4005B6处理后和601060比较即可。跟进4005B6:

    __int64 sub_4005B6()
    {
      __int64 result; // rax
      int i; // [rsp+0h] [rbp-10h]
      char v2; // [rsp+8h] [rbp-8h]
      int v3; // [rsp+Ch] [rbp-4h]
    
      for ( i = 0; i <= 14999; i += 3 )//i+=3,结合数据发现只执行了case1,2,3难度支线下降
      {
        v2 = byte_6010C0[i + 2];
        v3 = byte_6010C0[i + 1];
        result = (unsigned int)byte_6010C0[i];
        switch ( byte_6010C0[i] )
        {
          case 1:
            result = byte_6010C0[i + 1];
            *(&byte_604B80 + v3) += v2;
            break;
          case 2:
            result = byte_6010C0[i + 1];
            *(&byte_604B80 + v3) -= v2;
            break;
          case 3:
            result = byte_6010C0[i + 1];
            *(&byte_604B80 + v3) ^= v2;
            break;
          case 4:
            result = byte_6010C0[i + 1];
            *(&byte_604B80 + v3) *= v2;
            break;
          case 5:
            result = byte_6010C0[i + 1];
            *(&byte_604B80 + v3) ^= *(&byte_604B80 + byte_6010C0[i + 2]);
            break;
          default:
            continue;
        }
      }
      return result;
    }
    

    结合注释发现是要考虑加法,减法,和异或就好了,非常简单。

    这里采用idc脚本来解决,贴上idc脚本常用函数链接(已备份)

    #include<idc.idc>
    static main()//idc脚本标准开头
    {
    	auto i;//定义变量
    	for(i = 14997;i >= 0;i = i - 3)//这里需要逆向处理
    	{
    	    auto v0 = Byte(0x6010C0+i);//这里是从地址6010C0+i开始读取一个字节
    	    auto v3 = Byte(0x6010C0+(i + 2));
    	    auto result = v0;
    	    if(v0==1)
    		{
    	        result =Byte(0x6010C0+i + 1);
    	        PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)-v3);
    	    }//PatchByte(addr,val)表示把addr处的值修改为val
    	    if(v0==2)
    		{
    	        result =Byte(0x6010C0+i + 1);
    	        PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)+v3);
    	    }
    	    if(v0==3)
    		{
    	        result =Byte(0x6010C0+i + 1);
    	        PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)^v3);
    	    }
    	    if(v0==4)
    		{
    	        result =Byte(0x6010C0+i + 1);
    	        PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)/v3);
    	    }
    	    if(v0==5)
    		{
    	        result =Byte(0x6010C0+i + 1);
    	    	PatchByte(0x601060 + result*4,Byte(0x601060 + result*4)^Byte(0x601060+v3*4));
    	    }  
    	}
    	for(i=0;i<24;i++)
    	Message("%c",Byte(0x601060+i*4));
    }
    

    值得注意的是,byte_0x6010C0是char型也就是一个byte和byte_0x601060是四字节,因此result需要乘4保证addr的正确性。

    最后调用idc脚本即可。调用方法为:File->Script File->idc文件

    执行后得到flag:nctf{Embr4ce_Vm_j0in_R3}

    3.0 DDCTF黑盒测试(俩re手干了一天)

    3.1 源码分析

    解包后得到两个文件,一个elf,另一个txt文件,名字是flag-48ee204317,源代码:

    __int64 __fastcall main(int a1, char **a2, char **a3)
    {
      __int64 v4; // rbx
      int i; // [rsp+Ch] [rbp-84h]
      FILE *stream; // [rsp+10h] [rbp-80h]
      char s[8]; // [rsp+20h] [rbp-70h] BYREF
      int v8; // [rsp+28h] [rbp-68h]
      __int16 v9; // [rsp+2Ch] [rbp-64h]
      char v10; // [rsp+2Eh] [rbp-62h]
      char filename[8]; // [rsp+30h] [rbp-60h] BYREF
      __int64 v12; // [rsp+38h] [rbp-58h]
      __int64 v13; // [rsp+40h] [rbp-50h]
      int v14; // [rsp+48h] [rbp-48h]
      __int16 v15; // [rsp+4Ch] [rbp-44h]
      __int64 ptr[3]; // [rsp+50h] [rbp-40h] BYREF
      __int64 v17; // [rsp+68h] [rbp-28h]
      __int64 v18; // [rsp+70h] [rbp-20h]
      unsigned __int64 v19; // [rsp+78h] [rbp-18h]
    
      v19 = __readfsqword(0x28u);
      *(_QWORD *)filename = 0LL;
      v12 = 0LL;
      v13 = 0LL;
      v14 = 0;
      v15 = 0;
      *(_QWORD *)s = 0LL;
      v8 = 0;
      v9 = 0;
      v10 = 0;
      memset(ptr, 0, sizeof(ptr));
      v17 = 0LL;
      v18 = 0LL;
      puts("Please input your password!");//首先输入的是password
      fgets(s, 15, stdin);
      BYTE2(v8) = 0;
      for ( i = 0; i <= 9 && isalnum(s[i]); ++i )
        ;
      if ( i == 10 )
      {
        sprintf(filename, "flag-%s.txt", s);
          //通过这里可以知道,输入的s(也即是password就是txt附件的名称48ee204317)
        stream = fopen(filename, "r");
        if ( stream )
        {
          fread(ptr, 0x17uLL, 1uLL, stream);
          LOBYTE(v17) = 0;
          fclose(stream);
          puts("---------------------[Welcome To ReverseMe!]---------------------");
          puts("\n\nPlease Input Your Passcode,If You See print the \"Binggo\" string,Congratulations,You Win. Good luck!");
            //接下来输入的是passcode,经过分析其实就是虚拟机中的opcode,并且让程序输出Binggo,即可获得flag
          v4 = operator new(0xAA0uLL);
          sub_401E98(v4);
          if ( !(unsigned int)sub_4016BD(v4) )
          {
            printf("Error code:%x\n", (unsigned int)dword_6038E0);
            exit(0);
          }
          fgets((char *)(v4 + 16), 100, stdin);//这里可以看出passcode是从v4+16开始存储的
          if ( memcmp("exit", (const void *)(v4 + 16), 4uLL) )
          {
            sub_401A48(v4);//经过401A48的处理
            if ( byte_603F00 )//判定success的条件莫名其妙,使用交叉引用
              printf("Success!\nYour flag is %s\n", (const char *)ptr);
            else
              puts("Failed!");
          }
          sub_401B8B(v4);
          return 0LL;
        }
        else
        {
          puts("Could not read the flag! Please check your password.");
          return 1LL;
        }
      }
      else
      {
        puts("Invalid password!");
        return 1LL;
      }
    }
    

    sub_401E98是关键函数,步入之后的代码:

    __int64 __fastcall sub_401A48(__int64 a1)
    {
      char v2; // [rsp+13h] [rbp-1Dh]
      int i; // [rsp+14h] [rbp-1Ch]
      int j; // [rsp+18h] [rbp-18h]
    
      if ( a1 && a1 != -16 )
      {
        for ( i = 0; i < strlen((const char *)(a1 + 16)); ++i )
        {
          v2 = *(_BYTE *)(a1 + i + 16);//读取passcode的第i位
          if ( i + 1 != strlen((const char *)(a1 + 16)) )
            *(_BYTE *)(a1 + 664) = *(_BYTE *)(a1 + i + 1 + 16);//*(a1+664)=passcode[i+1]
          for ( j = 0; j <= 8; ++j )//说明有9种opcode
          {
            if ( byte_603900[v2] == *(_BYTE *)(a1 + *(int *)(a1 + 4 * (j + 72LL) + 8) + 408) )
            {//byte_603900显然需要dump出来
              *(_QWORD *)(a1 + 672) = *(_QWORD *)(a1 + 8 * (*(int *)(a1 + 4 * (j + 72LL) + 8) + 84LL) + 8);
              (*(void (__fastcall **)(__int64))(a1 + 672))(a1);
                //这里是一个fastcall,也是一种函数调用方式,很容易忽略,应该对应了汇编的call rax
            }
          }
        }
      }
      return 0LL;
    }
    

    乍一看其实比较难懂,此时结合汇编:

    注意到有个call rax,应该就是靠这个来实现跳转到各个handle内,而该函数应该就是Dispatcher,这里再回去看代码应该就很好理解了。

    3.2 动调找opcode

    有了上面的分析,下面开始进行动态调试,在byte_603900处下断点,在虚拟机中输入password绕过,再输入passcode:

    我们注意到,要想进行到跳转语句,必须满足表达式

     byte_603900[v2] == *(_BYTE *)(a1 + *(int *)(a1 + 4 * (j + 72LL) + 8) + 408) 
    

    而v2是我们输入的passcode,由此我们可以得出,byte_603900的作用应该是让opcode与我们输入的passcode进行一个一一对应的映射,此处我们在动态调试的情况下用idc脚本dump出符合所有$$ j=[0,8] $$ 的opcode,就可以找出我们所输入的passcode是由那些字符(opcode)组成的。

    我们在调用dispatch的地方下断点,F7步入后,调用idc脚本:

    #include<idc.idc>
    static main()
    {
    	auto j;
    	auto a1=0x17BA8B0;//此处的a1每次动态都不一样
    	for(j=0;j<=8;++j)
    	{
    		Message("0x%x,",Byte(a1+Byte(a1+4*(j+72)+8)+408));
        }
    }
    

    这样我们就可以得到opcode:0x2a,0x27,0x3e,0x5a,0x3f,0x4e,0x6a,0x2b,0x28

    接着再从ida扒出byte_603900的数据,找出映射之前组成passcode的字符:

    byte_603900 = [0x02, 0x00, 0x00, 0x0E, 0x16, 0x54, 0x20, 0x18, 0x11, 0x45,
    0x50, 0x59, 0x58, 0x53, 0x00, 0x08, 0x44, 0x2D, 0x46, 0x39,
    0x00, 0x54, 0x42, 0x01, 0x3C, 0x0F, 0x00, 0x07, 0x17, 0x00,
    0x56, 0x21, 0x00, 0x37, 0x6D, 0x2B, 0x2A, 0x6E, 0x59, 0x5D,
    0x47, 0x3A, 0x4A, 0x34, 0x44, 0x48, 0x43, 0x6C, 0x3F, 0x59,
    0x25, 0x33, 0x55, 0x2F, 0x31, 0x68, 0x27, 0x34, 0x7C, 0x28,
    0x67, 0x59, 0x00, 0x52, 0x00, 0x26, 0x00, 0x3E, 0x56, 0x4E,
    0x33, 0x21, 0x45, 0x6D, 0x60, 0x39, 0x46, 0x72, 0x6D, 0x4D,
    0x54, 0x40, 0x00, 0x74, 0x57, 0x73, 0x72, 0x7A, 0x47, 0x45,
    0x00, 0x71, 0x00, 0x4A, 0x35, 0x70, 0x3B, 0x36, 0x2E, 0x26,
    0x2C, 0x6C, 0x4A, 0x00, 0x7C, 0x63, 0x35, 0x57, 0x4D, 0x41,
    0x43, 0x62, 0x00, 0x68, 0x37, 0x00, 0x5A, 0x6A, 0x6B, 0x7C,
    0x29, 0x69, 0x4C, 0x70, 0x50, 0x71, 0x26, 0x36, 0x3C, 0x06,
    0x1B, 0x00, 0x3C, 0x30, 0x00, 0x00, 0x00, 0x4C, 0x0B, 0x4B,
    0x48, 0x08, 0x54, 0x47, 0x12, 0x09, 0x24, 0x00, 0x00, 0x24,
    0x40, 0x0D, 0x39, 0x06, 0x5C, 0x2C, 0x1A, 0x2D, 0x0A, 0x38,
    0x35, 0x37, 0x16, 0x3B, 0x00, 0x24, 0x48, 0x00, 0x49, 0x00,
    0x37, 0x08, 0x1F, 0x24, 0x45, 0x1D, 0x11, 0x40, 0x2F, 0x4A,
    0x08, 0x15, 0x00, 0x11, 0x00, 0x1A, 0x22, 0x41, 0x52, 0x5B,
    0x0B, 0x45, 0x31, 0x19, 0x43, 0x19, 0x1E, 0x0A, 0x21, 0x05,
    0x4D, 0x59, 0x38, 0x34, 0x09, 0x36, 0x2F, 0x43, 0x02, 0x53,
    0x12, 0x2F, 0x4C, 0x21, 0x0D, 0x3C, 0x31, 0x2E, 0x37, 0x08,
    0x30, 0x29, 0x32, 0x2F, 0x00, 0x1A, 0x14, 0x41, 0x53, 0x15,
    0x21, 0x00, 0x08, 0x13, 0x38, 0x5C, 0x36, 0x3B, 0x50, 0x00,
    0x2F, 0x1E, 0x57, 0x00, 0x30, 0x2E, 0x0C, 0x2E, 0x37, 0x52,
    0x1C, 0x33, 0x34, 0x11, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00]
    passcode = [0x2a, 0x27, 0x3e, 0x5a, 0x3f, 0x4e, 0x6a, 0x2b, 0x28]
    opcode = []
    for i in passcode:
        opcode.append(chr(byte_603900.index(i)))
    print(opcode)
    

    最终得到映射前的opcode:

    [’$’, ‘8’, ‘C’, ‘t’, ‘0’, ‘E’, ‘u’, ‘#’, ‘;’]
    

    3.3 再次动调跟踪每个opcode对应的case

    有了opcode,接下来的工作是跟踪每一个opcode对应的case。

    通过对byte_603F00的交叉引用,找到其直接调用的函数

    我们发现该函数是通过fastcall进行调用的,说明该函数应该就是其中的一个case,我们再对sub_401D33D进行交叉引用:

    找到了每个case对应的函数。

    接下来,我们通过动态调试,在输入passcode的地方输入之前得到的opcode,此处仅以opcode('8')演示

    在此处下断点后,if语句种的条件应当是成立的,继续F8,在fastall的地方F7步入,我们就可以找到opcode(8)对应的case

    用同样的方法,我们可以找到每个opcode对应的case:

    $ -> sub_400DC1(func1)
    8 -> sub_400E7A(func2)
    C -> sub_400F3A(func3)
    t -> sub_401064(func4)
    0 -> sub_4011C9(func5)
    E -> sub_40133D(func6)
    u -> sub_4012F3(func7)
    # -> sub_4014B9(func8)
    ; -> sub_400CF1(func9)
    

    3.4 对每个case的功能进行分析

    接下来是SG大爹的硬核分析环节:

    我们先把伪代码进行初步的简化

    func1:
    	if *(a1 + 288) < *(a1 + 292)
    		*(a1 + 665) = *((a1 + 8) + (int)(a1 + 288));
    
    func2:
    	if *(a1 + 288) < *(a1 + 292)
            *((int)(a1 + 288) + (a1 + 8)) = *(a1 + 665)
    
    func3:
    	if *(a1 + 288) < *(a1 + 292)
    		*(a1 + 665) = *(a1 + 665) + *(a1 + 664) - 33
    
    func4
    	if *(a1 + 288) < *(a1 + 292)
    		*(_BYTE *)(a1 + 665) = *(_BYTE *)(a1 + 665) - *(_BYTE *)(a1 + 664) + 33
    		if (a1 + 665)
    			++(a1 + 665)
    
    func5:
    	if *(a1 + 288) < *(a1 + 292)
    		++*(a1 + 288)
    
    func6:
    	check()
    
    func7:
    	if *(a1 + 288) > 0
    		--*(a1 + 288)
    
    func8:
    	++(a1 + 288)
    	if *(a1 + 288) < *(a1 + 292) and *(a1 + 664) - 48 <= 41
    		*((a1 + 280) + (int)(a1 + 288) + a1) = *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48) - 49
    
    func9:
    	for (i = 0; i < *(a1 + 664); ++i)
        	++*(a1 + 288);
        if *(a1 + 664) - 16 <= 89
        	*((a1 + 280) + (int)(a1 + 288) + a1) = *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48) - 49
        	
    check()
    	if (a1 + 664) == 's'
    		s = ((a1 + 8) + (a1 + 288)), len = 20
    		if (right(s))
    			success!
    

    其中,check()的逻辑是

    if (a1 + 664) == 's'
    		s = ((a1 + 8) + (a1 + 288)), len = 20
    		if (right(s))
    			success!
    

    然后进行动态调试,可以发现

    • +655: 临时变量tmp
    • +8: str = "PaF0!&Prv}H{ojDQ#7v="
    • +288: pt
    • +16: 我们输入的passcode(input)
    • +664: 下一位输入(next, 即input[i + 1])

    并且,一些奇怪的东西如下:

    • *((a1 + 280) + (int)(a1 + 288) + a1): str[pt]
    • *((a1 + 16) + *(a1 + 288) + *(a1 + 664) - 48): input[pt + input[i] - 48]

    那么,可以拿出伪代码的进一步分析了

    func1($):
    	temp = str[pt]
    func2(8):
    	str[pt] = temp
    func3(C):
    	temp = temp + next - 33
    func4(t):
    	temp = temp - next + 33
    func5(0):
    	++pt
    func6(E):
    	check()
    func7(u):
    	--pt
    func8(#):
    	str[pt] = input[pt + next - 48] - 49
    func9(;):
    	for (i = 0; i < next; ++i) ++pt;
    	str[pt] = input[pt + next - 48] - 49
    

    并且,在check()中,我们还发现:程序的目的是将str = "PaF0!&Prv}H{ojDQ#7v=" 转换成 str = "Binggo",然后我们才可以得到flag

    显然地,str的位数远大于"Binggo",我们得考虑在PaF0!&P的第二个P处用'\0'将字符串截断

    3.5 构造passcode使程序输出Binggo

    从头开始构造吧

    首先看P -> B,我们先取出'P',用'$'使得temp = str[pt],此时pt = 0,则temp = 'P'

    两者的ASCII分别是80, 66,考虑用't操作'

    \(80 - next + 33 = 66\),显然\(next = 47\),即'/'

    然后分别用'8''0'来进行更新,并且将pt指针后移一位

    综上,第一步的opcode应该是$t/80

    同理,PaF0!&转换成Binggo需要的opcode应该是$t/80$C)80$CI80$CX80$Cg80$Cj80

    现在该考虑截断操作了

    这里我们不能用func3来构造了,这样得到的是一个不可见字符

    那只能考虑func8来进行一下复杂操作了

    我们考虑在P -> B的时候额外插入一个111来让pt指向第七个字符时直接构造'\0',最后调整pt到str前端,最后用'E'调用check()并用's'满足check()的条件

    最终的passcode就是

    $t/81110$C)80$CI80$CX80$Cg80$Cj80#0uuuuuuuuEs

    3.6 提交passcode得到flag

  • 相关阅读:
    PHP二维数组排序(感谢滔哥lvtao.net)
    MySQL 日志的类型
    PHP安装Xdebug扩展并配置PHPstorm调试(Centos、Windows)
    $.post 和 $.get 设置同步和异步请求
    Jquery获取敲击回车时光标所在的位置
    Jquery 数组与字符串之间的转换
    使整个页面变灰的css代码
    PHP的性能优化方法总结
    Android中完全退出当前应用系统
    Android 遮罩层效果
  • 原文地址:https://www.cnblogs.com/THRANDUil/p/16074546.html
Copyright © 2020-2023  润新知