buuctf_Reverse_wp_2

文章目录

    • [WUSTCTF2020]level3[base64变表]
    • Youngter-drive[upx、多线程]
    • [FlareOn4]IgniteMe[算法分析]
    • 相册[APK、so文件、Native方法]
    • [WUSTCTF2020]Cr0ssfun[套娃、patience]
    • [GWCTF 2019]xxor[z3、算法分析]
    • [UTCTF2020]basic-re
    • [FlareOn6]Overlong
      • 脚本输出
      • 动态调试
    • [FlareOn3]Challenge1[base64变表]
    • 特殊的BASE64[base64变表]
    • [ACTF新生赛2020]Oruga[maze]
    • [ACTF新生赛2020]Universe_final_answer[z3]
    • [BJDCTF2020]BJD hamburger competition[Unity逆向、C#逆向]
    • [Zer0pts2020]easy strcmp[算法分析、init段、hook]
    • [WUSTCTF2020]level4[DS、二叉树遍历]
    • [羊城杯 2020]easyre[算法逆向、Caesar]
    • [网鼎杯 2020 青龙组]singal[算法分析,虚拟机指令、angr]
      • 正常解法:
      • 利用Python angr库

[WUSTCTF2020]level3[base64变表]

64位elf文件,拖进ida64看一下,定位到main函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  const char *v3; // rax
  char v5; // [rsp+Fh] [rbp-41h]
  char v6[56]; // [rsp+10h] [rbp-40h] BYREF
  unsigned __int64 v7; // [rsp+48h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  printf("Try my base64 program?.....\n>");
  __isoc99_scanf("%20s", v6);
  v5 = time(0LL);
  srand(v5);
  if ( (rand() & 1) != 0 )
  {
    v3 = base64_encode(v6);
    puts(v3);
    puts("Is there something wrong?");
  }
  else
  {
    puts("Sorry I think it's not prepared yet....");
    puts("And I get a strange string from my program which is different from the standard base64:");
    puts("d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD==");
    puts("What's wrong??");
  }
  return 0;
}

看到一串base64,解密发现是乱码,那就证明所给的这一串字符串有问题,但是唯一能更改密文的就是那个base64_encode函数了,再结合题目中说的与标准base64不一样的,就考虑到是变表问题。

然后突然看到有个函数也引用了这个函数,定位一下

__int64 O_OLookAtYou()
{
  __int64 result; // rax
  char v1; // [rsp+1h] [rbp-5h]
  int i; // [rsp+2h] [rbp-4h]

  for ( i = 0; i <= 9; ++i )
  {
    v1 = BASE64_table_6020A0[i];
    BASE64_table_6020A0[i] = BASE64_table_6020A0[19 - i];
    result = 19 - i;
    BASE64_table_6020A0[result] = v1;
  }
  return result;
}

找到了具体规则,先用脚本跑一下,将变表输出来,然后将不规则的base64密文通过变表还原

BASE64_table_6020A0 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
base64_table = [ch for ch in BASE64_table_6020A0]
base64_string = "d2G0ZjLwHjS7DmOzZAY0X2lzX3CoZV9zdNOydO9vZl9yZXZlcnGlfD=="
print(base64_table)
for i in range(10):
    tmp =  base64_table[i]
    base64_table[i] = base64_table[19 - i]
    res = 19 - i
    base64_table[res] = tmp
print(base64_table)#输出变表,然后将对应的字符替换即可
exbase64_table = "TSRQPONMLKJIHGFEDCBAUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
real_base64cipher = ""
for j in base64_string:
     if j == "=":
        real_base64cipher += j #将等号也输出
        continue
    index = exbase64_table.find(j)
    real_base64cipher += BASE64_table_6020A0[index]
print(real_base64cipher)
#d2N0ZjIwMjB7QmFzZTY0X2lzX3RoZV9zdGFydF9vZl9yZXZlcnNlfQ==

在这里插入图片描述

或者在线自定义base64加解密也可以
在这里插入图片描述

Youngter-drive[upx、多线程]

32位带upx壳,首先脱壳

D:\tools\re\upx-4.2.2-win64\upx-4.2.2-win64>upx.exe -d D:\ctf\buuctf\7289daa8-a5d5-430e-87a0-92ce805d8f15\Youngter-drive.exe

拖入IDA,发现只是一些创建互斥量句柄的函数,然后将输入source复制给destination

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
  HANDLE Thread; // [esp+D0h] [ebp-14h]
  HANDLE hObject; // [esp+DCh] [ebp-8h]

  (sub_4110FF)();
  ::hObject = CreateMutexW(0, 0, 0);
  j_strcpy(Destination, &Source);
  hObject = CreateThread(0, 0, StartAddress, 0, 0, 0);
  Thread = CreateThread(0, 0, sub_41119F, 0, 0, 0);
  CloseHandle(hObject);
  CloseHandle(Thread);
  while ( dword_418008 != -1 )
    ;
  sub_411190();
  CloseHandle(::hObject);
  return 0;
}

定位多线程中的函数StartAddress,首先代码会让刚才多线程创建的句柄hObject处于无限期的等待中直到线程结束,然后判断dword_418008是否大于-1,初始是29。然后会执行函数sub_41112C,最后释放互斥锁

void __stdcall __noreturn StartAddress_0(int a1)
{
  while ( 1 )
  {
    WaitForSingleObject(hObject, 0xFFFFFFFF);
    if ( dword_418008 > -1 )
    {
      sub_41112C(Source, dword_418008);
      --dword_418008;
      Sleep(0x64u);
    }
    ReleaseMutex(hObject); //释放资源以便其他线程共享,避免死锁
  }
}

看一下sub_41112C函数

char *__cdecl sub_411940(int a1, int a2)
{
  char *result; // eax
  char v3; // [esp+D3h] [ebp-5h]

   //off_418000 = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasd"
  v3 = *(a2 + a1); //v3 = Source[dword_418008]
  if ( (v3 < 'a' || v3 > 'z') && (v3 < 'A' || v3 > 'Z') )
    exit(0);
  if ( Source[dword_418008] < 'a' || Source[dword_418008] > 'z' ) 
  {
    result = off_418000[0];
    Source[dword_418008] = off_418000[0][Source[dword_418008] - 38]; //v3 = off_418000[source[dword_418008]-38]
  }
  else
  {
    result = off_418000[0];
    Source[dword_418008] = off_418000[0][Source[dword_418008] - 96];
  }
  return result;
}

再定位到多线程的第二个函数,它的逻辑与第一个类似,只是没有了其他操作,但是看似没操作实际上它对dword_418008进行了减一操作,导致上一个函数进行操作的时候是按照奇数个操作的,因为这是第二个函数,所以第一个函数会先执行一次,然后每隔一个进行一次加解密操作,例如最开始,下标29会先执行加密函数,然后计数器减一等于28,到了第二个函数,只会减掉计数器,所以此时计数器为27,就又会执行加密函数,以此类推,只有下标为奇数的时候才会加密,偶数不加密

void __stdcall __noreturn sub_411B10(int a1)
{
  while ( 1 )
  {
    WaitForSingleObject(hObject, 0xFFFFFFFF);
    if ( dword_418008 > -1 )
    {
      Sleep(0x64u);
      --dword_418008;
    }
    ReleaseMutex(hObject);
  }
}

最后看一下比较函数,只要最后的结果等于那一串字符串就行也就是Source数组的前29位是那一串字符串

int sub_411880()
{
  int i; // [esp+D0h] [ebp-8h]

  for ( i = 0; i < 29; ++i )
  {
    if ( Source[i] != off_418004[i] )
      exit(0);
  }
  return printf("\nflag{%s}\n\n", Destination);
}//off_418004 = TOiZiZtOrYaToUwPnToBsOaOapsyS

最后再看一下flag总共多少位,我们可以看到输入是36位,去掉flag{}共有30位,但是计数器只有29位,这就要让我们爆破一位

在这里插入图片描述

所以我们写一个脚本

Source="TOiZiZtOrYaToUwPnToBsOaOapsyS"
off_418000 = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm"
flag = ''
for i in range(len(Source)):
    if i % 2 == 1: #表示下标为奇数加密
        ch = Source[i]
        if ch < 'a' or ch >'z': #如果加密以后的密文是大写则表明明文是小写的
            index = off_418000.find(Source[i])
            flag += chr( 96 + index )
        else: #或者直接用xxx.isupper()函数直接判断是不是大写
            index = off_418000.find(Source[i])
            flag += chr( 38 + index)
    else:#偶数不加密
        flag += Source[i]
print("flag{" + flag +"E}")     

最后因为我在buu上写的所以最后加一个E即可得到flag

[FlareOn4]IgniteMe[算法分析]

32位exe,拖入ida看看,找到start函数发现只要满足if条件即可也即函数sub_401050返回值为1即可

void __noreturn start()
{
  DWORD NumberOfBytesWritten; // [esp+0h] [ebp-4h] BYREF

  NumberOfBytesWritten = 0; 
  hFile = GetStdHandle(0xFFFFFFF6); //获取标准输入句柄
  dword_403074 = GetStdHandle(0xFFFFFFF5); //获取标准输出句柄
  WriteFile(dword_403074, aG1v3M3T3hFl4g, 0x13u, &NumberOfBytesWritten, 0); //将aG1v3M3T3hFl4g写入标准输出设备也就是控制台
  sub_4010F0(NumberOfBytesWritten); //处理输入的数据
  if ( sub_401050() )
    WriteFile(dword_403074, aG00dJ0b, 0xAu, &NumberOfBytesWritten, 0);
  else
    WriteFile(dword_403074, aN0tT00H0tRWe7r, 0x24u, &NumberOfBytesWritten, 0);
  ExitProcess(0);
}

在中间有一个函数 sub_4010F0就是用来处理我们输入的数据

int sub_4010F0()
{
  unsigned int v0; // eax
  char Buffer[260]; // [esp+0h] [ebp-110h] BYREF
  DWORD NumberOfBytesRead; // [esp+104h] [ebp-Ch] BYREF
  unsigned int i; // [esp+108h] [ebp-8h]
  char v5; // [esp+10Fh] [ebp-1h]

  v5 = 0;
  for ( i = 0; i < 260; ++i )
    Buffer[i] = 0; //初始化缓冲区都为0
  ReadFile(hFile, Buffer, 0x104u, &NumberOfBytesRead, 0);
   //用来从键盘中读取数据大小是240个字节
  for ( i = 0; ; ++i )
  {
    v0 = sub_401020(Buffer); //sub_401020函数只是确定我们缓冲区的有效字节数,通俗一点就是数组长度
    if ( i >= v0 )
      break;
    v5 = Buffer[i];
    if ( v5 != 10 && v5 != 13 ) //判断如果buffer里面的字符不是换行符且不为空的时候
    {
      if ( v5 )
        byte_403078[i] = v5; //用byte_403078这个数组接受从键盘输入的数据最多260字节
    }
  }
  return 1;
}

继续看函数sub_401050

int sub_401050()
{
  int v1; // [esp+0h] [ebp-Ch]
  int i; // [esp+4h] [ebp-8h]
  unsigned int j; // [esp+4h] [ebp-8h]
  char v4; // [esp+Bh] [ebp-1h]

  v1 = sub_401020(byte_403078); //确定输入数据的长度
  v4 = sub_401000();
  for ( i = v1 - 1; i >= 0; --i ) //从输入的最后一个开始往前循环
  {
    byte_403180[i] = v4 ^ byte_403078[i]; //将刚才输入数据的最后一位以此往前与v4相异或
    v4 = byte_403078[i]; //从第一循环结束以后,v4就是前一位数据
  }
  for ( j = 0; j < 39; ++j )
  {
    if ( byte_403180[j] != byte_403000[j] )
      return 0;
  }
  return 1;
}

其中函数sub_401000()让0x80070000循环左移4位以后,再右移一位,得到0x70000004,但是注意因为返回值的数据类型是int16所以就占两个字节,故实际结果v4=0x0004也就是4

在这里插入图片描述

所以根据分析写一个脚本

 byte_403180=[0x0D, 0x26, 0x49, 0x45, 0x2A, 0x17, 0x78, 0x44, 0x2B, 0x6C, 
  0x5D, 0x5E, 0x45, 0x12, 0x2F, 0x17, 0x2B, 0x44, 0x6F, 0x6E, 
  0x56, 0x09, 0x5F, 0x45, 0x47, 0x73, 0x26, 0x0A, 0x0D, 0x13, 
  0x17, 0x48, 0x42, 0x01, 0x40, 0x4D, 0x0C, 0x02, 0x69]
v4 = 4
flag=''
for i in range(38,-1,-1):#直接倒序输出flag最后再逆序即可
    tmp = v4 ^ byte_403180[i]
    flag += chr(tmp)
    v4 = tmp  #v4每次等于上一次的结果
print("flag{"+ flag[::-1] + "}")

在这里插入图片描述

这道题其实只要明白ROL_4的含义以及它的数据类型是int16就够了,其他就是一些基本的算法逆向

相册[APK、so文件、Native方法]

下载以后发现是个apk包,用jadx打开,发现一堆java代码,根本无从下手,但是题目提示我们要提取完整邮箱即为flag。所以就知道这个关键函数或者叫做方法,肯定与mail有关

Ctrl+Shift+F搜索main,发现一个名字叫sendMailByJavaMail的函数

在这里插入图片描述

打开看一下,发现有successful等字样,与常规ctf提示信息类似,那就更加确定了这里

在这里插入图片描述

继续定位一下C2类,看到里面的邮箱、用户名、密码等有关信息,且都是由base64加密的

在这里插入图片描述

继续跟进NativeMethod方法,发现是用native关键字定义的,那就代表了用到了外部链接库

uuRE%5Cimage-20240313104135559.png&pos_id=img-J6uq2hw6-1710312799920)

在Java中,native关键字主要与本地方法(native method)相关。本地方法是用非Java语言(如C、C++等)编写的,并通过Java的native接口在Java程序中调用。这允许Java代码与本地应用程序或库进行交互,从而扩展了Java的能力。

所以去lib文件夹下看看有没有什么东西,找到了一个.so文件,也就是一个动态链接库文件

在这里插入图片描述

.so文件是Linux下的程序函数库,也被称为动态链接库文件。它是一种编译后的二进制文件,包含了可被程序动态加载的代码和数据。由于Android操作系统的底层基于Linux系统,所以.so文件可以运行在Android平台上。在Android开发中,使用C/C++接口开发Native程序时,经常需要将核心代码以.so文件的形式提供,以提高性能和安全性。

既然是由C/C++开发的,那么就可以用ida分析一下。具体方法是将.apk重命名为.zip再打开lib文件夹找到.so文件

定位到刚才对邮箱加密的函数,解密一下即可
在这里插入图片描述

import base64
cipher = "MTgyMTg0NjUxMjVAMTYzLmNvbQ=="
res = base64.b64decode(cipher).decode()
print("flag{"+res+"}")

[WUSTCTF2020]Cr0ssfun[套娃、patience]

64位elf文件,定位到main函数中的关键函数check,发现里面像是套娃一样,一堆函数。最后要求返回1

check -> iven_is_handsome -> iven_is_c0ol -> iven_1s_educated -> iven_1s_brave -> iven_1s_great -> iven_and_grace -> finally_fun

_BOOL8 __fastcall iven_is_handsome(_BYTE *a1)
{
  return a1[10] == 'p' && a1[13] == '@' && a1[3] == 'f' && a1[26] == 'r' && a1[20] == 'e' && iven_is_c0ol(a1);
}
_BOOL8 __fastcall iven_is_c0ol(_BYTE *a1)
{
  return a1[7] == '0' && a1[16] == '_' && a1[11] == 'p' && a1[23] == 'e' && a1[30] == 'u' && iven_1s_educated(a1);
}
_BOOL8 __fastcall iven_1s_educated(_BYTE *a1)
{
  return *a1 == 'w' && a1[6] == '2' && a1[22] == 's' && a1[31] == 'n' && a1[12] == '_' && iven_1s_brave(a1);
}
_BOOL8 __fastcall iven_1s_brave(_BYTE *a1)
{
  return a1[15] == 'd' && a1[8] == '{' && a1[18] == '3' && a1[28] == '_' && a1[21] == 'r' && iven_1s_great(a1);
}
_BOOL8 __fastcall iven_1s_great(_BYTE *a1)
{
  return a1[2] == 't' && a1[9] == 'c' && a1[32] == '}' && a1[19] == 'v' && a1[5] == '0' && a1[14] == 'n' && iven_and_grace(a1);
}
_BOOL8 __fastcall iven_and_grace(_BYTE *a1)
{
  return a1[4] == '2' && a1[17] == 'r' && a1[29] == 'f' && a1[17] == 'r' && a1[24] == '_' && finally_fun(a1);
}
_BOOL8 __fastcall finally_fun(_BYTE *a1)
{
  return a1[1] == 'c' && a1[25] == '@' && a1[27] == 'e';
}

根据上面的函数调用关系可以推出flag总共33个字符,所以组合一下即可得到flag

wctf2020{cpp_@nd_r3verse_@re_fun}

这道题就是麻烦。。。。纯属搞心态的,也可以写个脚本从0到32输出即可,我是一个一个比的

[GWCTF 2019]xxor[z3、算法分析]

64位elf文件,拖入ida64看看,定位到main函数

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+8h] [rbp-68h]
  int j; // [rsp+Ch] [rbp-64h]
  __int64 v6[6]; // [rsp+10h] [rbp-60h] BYREF
  __int64 v7[6]; // [rsp+40h] [rbp-30h] BYREF

  v7[5] = __readfsqword(0x28u);
  puts("Let us play a game?");
  puts("you have six chances to input");
  puts("Come on!");
  memset(v6, 0, 40);
  for ( i = 0; i <= 5; ++i ) 
  {
    printf("%s", "input: ");
    a2 = (v6 + 4 * i); //这里直接看为a2 = v6[i]即可,因为int就是4字节,而i是一个字节
    __isoc99_scanf("%d", a2);
  }
  memset(v7, 0, 40);
  for ( j = 0; j <= 2; ++j )
  {
    dword_601078 = v6[j]; //获取到v6的低32位
    dword_60107C = HIDWORD(v6[j]); //获取高32位其实可以理解为v6[j+1],因为j正好是从0到3
    a2 = &asc_601060; // asc_601060="2,2,3,4"
    sub_400686(&dword_601078, &asc_601060);
    LODWORD(v7[j]) = dword_601078; //通过sub_400686函数已经变了
    HIDWORD(v7[j]) = dword_60107C; //v7的高32位与v6的高32位一样并没有变化
  }
  if ( sub_400770(v7, a2) != 1 )
  {
    puts("NO NO NO~ ");
    exit(0);
  }
  puts("Congratulation!\n");
  puts("You seccess half\n");
  puts("Do not forget to change input to hex and combine~\n");
  puts("ByeBye");
  return 0LL;
}

其中有个sub_400686它对两个数组dword_601078和asc_601060,进行了变化。所以跟进一下

__int64 __fastcall sub_400686(unsigned int *a1, _DWORD *a2)
{ //此时a1 =  dword_601078(空数组)开头输入的flag的低32位  a2 = asc_601060 = 2,2,3,4
  __int64 result; // rax
  unsigned int v3; // [rsp+1Ch] [rbp-24h]
  unsigned int v4; // [rsp+20h] [rbp-20h]
  int v5; // [rsp+24h] [rbp-1Ch]
  unsigned int i; // [rsp+28h] [rbp-18h]

  v3 = *a1;
  v4 = a1[1];
  v5 = 0;
  for ( i = 0; i <= 63; ++i )
  {
    v5 += 0x458BCD42;
    v3 += (v4 + v5 + 11) ^ ((v4 << 6) + *a2) ^ ((v4 >> 9) + a2[1]) ^ 0x20;
    v4 += (v3 + v5 + 20) ^ ((v3 << 6) + a2[2]) ^ ((v3 >> 9) + a2[3]) ^ 0x10;
  }
  *a1 = v3;
  result = v4;
  a1[1] = v4;	
  return result;
}

还有一个比较函数sub_400770,只要传入的参数也就是v7的值与下列符合即表示正确

__int64 __fastcall sub_400770(_DWORD *a1)
{
  if ( a1[2] - a1[3] == 0x84A236FFLL
    && a1[3] + a1[4] == 0xFA6CB703LL
    && a1[2] - a1[4] == 0x42D731A8LL
    && *a1 == 0xDF48EF7E
    && a1[5] == 0x84F30420
    && a1[1] == 0x20CAACF4 )
  {
    puts("good!");
    return 1LL;
  }
  else
  {
    puts("Wrong!");
    return 0LL;
  }
}

所以就先用从[GUET-CTF2019]re学到的z3约束器来解一下比较的那个方程,算一下a1的每个元素,实质上也是v7的每个元素

from z3 import *

z = Solver()
a1 = [0]*6
for i in range(6):
    a1[i] = Int('a1[' + str(i) + ']') # 包含 6 个 Z3 整数符号的列表 a1
 
z.add(a1[2] - a1[3] == 0x84A236FF)
z.add(a1[3] + a1[4] == 0xFA6CB703)
z.add(a1[2] - a1[4] == 0x42D731A8)
z.add(a1[0] == 0xDF48EF7E)
z.add(a1[5] == 0x84F30420)
z.add(a1[1] == 0x20CAACF4)

z.check()
print(z.model())

#a1[4] = 2652626477
#a1[1] = 550153460
#a1[3] = 1548802262
#a1[5] = 2230518816
#a1[2] = 3774025685
#a1[0] = 3746099070

最后用脚本逆一下那个转换算法即可得到原来输入的内容。我们首先正向分析一遍,代码首先对我们的输入进行分块输入也就是分为低32位与高32位,所以这也解释了为什么我们输入了6次,但是在进行加密操作的时候只循环3次。对于加密函数来说,传入的参数a1(v3)也就是v6[j]也就是输入数据的低32位,而v4=a1[1]其实也就是v6[j+1]也就是输入的高32位。所以外层循环就是保证每次加密的是一个64位数据,也就是2个32位数据。而内层循环就是对这个64位数据依次进行加密。所以与其说输入6次,不如说输入6次32位数据,也就是32位数据。

所以写脚本的时候只要逆着写即可,里面的+=换为-=即可,v5从最后一个累加开始

#include<stdio.h>

int main()
{
	int  v7[6] = {3746099070,550153460,3774025685,1548802262,2652626477,2230518816};
	unsigned int v3,v4; //分别代表v7中的低32位与高32位
	 
	//这里如果直接用int就超出范围了,所以直接用unsigned int
	unsigned int a2[4] = {2,2,3,4};
	unsigned int i,j;
	int v5;
	
	for ( i = 0; i < 5; i+=2 )//外层循环,用来分别加密第一二三组64位数
	{
		v5 =  1166789954 * 64;// 因为v5每次大循环结束后会累加64次
		v3 = v7[i];
		v4 = v7[i+1];
		for (j = 0; j < 64; j++)
		{
			v4 -= (v3 + v5 + 20) ^ ((v3 << 6) + a2[2]) ^ ((v3 >> 9) + a2[3]) ^ 0x10;
			v3 -= (v4 + v5 + 11) ^ ((v4 << 6) + *a2) ^ ((v4 >> 9) + a2[1]) ^ 0x20;
			v5 -= 1166789954;
		}
		v7[i] = v3;
		v7[i+1] = v4;
	}
	for (i = 0; i < 6; i++)
	{
		printf("%c%c%c", *((char*)&v7[i] + 2), *((char*)&v7[i] + 1), *(char*)&v7[i]);
	}
    //(char*)(&v7[i])用来获得通过强制类型转换的字符指针也就是多个字节(小端),最后直接得到值转换为小端
    return 0;
} 

在这里插入图片描述

[UTCTF2020]basic-re

64位elf文件

这道题能投机取巧,直接Shift+F12就看到了flag…

在这里插入图片描述

然后我抱着学习的心态想做一下这道题,然后发现实在是没有什么可以学习的,光看汇编就会直接跳转到flag那里只是程序没有print出来,如果动态调试的话,程序最后看一下eax寄存器的值即可

在这里插入图片描述

[FlareOn6]Overlong

32位exe,一开始看到这个程序感觉好简单啊就那么几行的代码,但是发现有个一个数组unk_402008非常长,本来想写脚本的发现太长了就不想写了,最后看了一下wp有人od动态调试弄出来,有人写脚本也写出来了,我们两种方法都用。首先main函数就是一个弹窗的函数,其中注意一点就是对于v6的值,其实是由sub_401160函数的第三个参数决定的也就是28,所以第二行让Text[28]=0,实际上就是让Text只有28个字符,因为第29个是字符串终止符,所以MessageBoxA也就只能输出28个字符,也就是我们运行程序所得到的结果,所以其实想到这里也能知道后面还有东西但是题目中限制了28了,所以动态调试更改输出限制也是做题的关键。

在这里插入图片描述

在这里插入图片描述

int __stdcall start(int a1, int a2, int a3, int a4)
{
  CHAR Text[128]; // [esp+0h] [ebp-84h] BYREF
  int v6; // [esp+80h] [ebp-4h]

  v6 = sub_401160(Text, &unk_402008, 28);
  Text[v6] = 0;
  MessageBoxA(0, Text, Caption, 0);
  return 0;
}

先定位sub_401160函数,发现里面还有一个函数再看一下,可以看到最后return的i就是a3的大小,也即28

unsigned int __cdecl sub_401160(char *a1, int a2, unsigned int a3)
{
  unsigned int i; // [esp+4h] [ebp-4h]

  for ( i = 0; i < a3; ++i )
  {
    a2 += sub_401000(a1, a2);
    if ( !*a1++ ) //到了a1的结尾就停止
      break;
  }
  return i;
}

定位函数sub_401000,好像是一个类似加密的函数,其中a1和a2对应的就是函数sub_401160中的a1和a2,也就是程序最开始的Text与一大串字符串unk_402008进行异或移位操作。

int __cdecl sub_401000(_BYTE *a1, char *a2)
{
  int v3; // [esp+0h] [ebp-8h]
  char v4; // [esp+4h] [ebp-4h]

  if ( *a2 >> 3 == 30 )
  {
    v4 = a2[3] & 0x3F | ((a2[2] & 0x3F) << 6);
    v3 = 4;
  }
  else if ( *a2 >> 4 == 14 )
  {
    v4 = a2[2] & 0x3F | ((a2[1] & 0x3F) << 6);
    v3 = 3;
  }
  else if ( *a2 >> 5 == 6 )
  {
    v4 = a2[1] & 0x3F | ((*a2 & 0x1F) << 6);
    v3 = 2;
  }
  else
  {
    v4 = *a2;
    v3 = 1;
  }
  *a1 = v4;
  return v3;
}

脚本输出

首先写一个脚本看看什么情况

a2 = [0xE0, 0x81, 0x89, 0xC0, 0xA0, 0xC1, 0xAE, 0xE0, 0x81, 0xA5, 
  0xC1, 0xB6, 0xF0, 0x80, 0x81, 0xA5, 0xE0, 0x81, 0xB2, 0xF0, 
  0x80, 0x80, 0xA0, 0xE0, 0x81, 0xA2, 0x72, 0x6F, 0xC1, 0xAB, 
  0x65, 0xE0, 0x80, 0xA0, 0xE0, 0x81, 0xB4, 0xE0, 0x81, 0xA8, 
  0xC1, 0xA5, 0x20, 0xC1, 0xA5, 0xE0, 0x81, 0xAE, 0x63, 0xC1, 
  0xAF, 0xE0, 0x81, 0xA4, 0xF0, 0x80, 0x81, 0xA9, 0x6E, 0xC1, 
  0xA7, 0xC0, 0xBA, 0x20, 0x49, 0xF0, 0x80, 0x81, 0x9F, 0xC1, 
  0xA1, 0xC1, 0x9F, 0xC1, 0x8D, 0xE0, 0x81, 0x9F, 0xC1, 0xB4, 
  0xF0, 0x80, 0x81, 0x9F, 0xF0, 0x80, 0x81, 0xA8, 0xC1, 0x9F, 
  0xF0, 0x80, 0x81, 0xA5, 0xE0, 0x81, 0x9F, 0xC1, 0xA5, 0xE0, 
  0x81, 0x9F, 0xF0, 0x80, 0x81, 0xAE, 0xC1, 0x9F, 0xF0, 0x80, 
  0x81, 0x83, 0xC1, 0x9F, 0xE0, 0x81, 0xAF, 0xE0, 0x81, 0x9F, 
  0xC1, 0x84, 0x5F, 0xE0, 0x81, 0xA9, 0xF0, 0x80, 0x81, 0x9F, 
  0x6E, 0xE0, 0x81, 0x9F, 0xE0, 0x81, 0xA7, 0xE0, 0x81, 0x80, 
  0xF0, 0x80, 0x81, 0xA6, 0xF0, 0x80, 0x81, 0xAC, 0xE0, 0x81, 
  0xA1, 0xC1, 0xB2, 0xC1, 0xA5, 0xF0, 0x80, 0x80, 0xAD, 0xF0, 
  0x80, 0x81, 0xAF, 0x6E, 0xC0, 0xAE, 0xF0, 0x80, 0x81, 0xA3, 
  0x6F, 0xF0, 0x80, 0x81, 0xAD]#足足有176字节

text = ''
v3 = 0
for i in range(68):
    if ( a2[0+v3] >> 3 == 30 ):
        v4 = a2[3+v3] & 0x3F | ((a2[2+v3] & 0x3F) << 6)
        v3 += 4
	elif ( a2[0+v3] >> 4 == 14 ):
        v4 = a2[2+v3] & 0x3F | ((a2[1+v3] & 0x3F) << 6)
        v3 += 3
    elif ( a2[0+v3] >> 5 == 6 ):
        v4 = a2[1+v3] & 0x3F | ((a2[0+v3] & 0x1F) << 6)
        v3 += 2
    else:
        v4 = a2[0+v3]
        v3 += 1
	text += chr(v4)
print(text)

在这里插入图片描述

可以看到输出的内容与我们刚启动程序一致,所以我们只要把循环改大就可以得到正确结果比如说是68,其实我也不知道为什么到底是68,网上有一篇博客说是text段只有68个字符,但是其实我也没懂,我看到text的的大小是127啊…

在这里插入图片描述

动态调试

直接用od打开,把传入函数的第三个参数改大一点,就可以让其全部输出了。

在这里插入图片描述

在这里插入图片描述

[FlareOn3]Challenge1[base64变表]

32位exe文件,定位到main函数,发现逻辑比较简单和[FlareOn4]IgniteMe题的代码差不多,就是创建两个句柄,分别为标准输入和标准输出。最后只要比较输入的字符串经过函数sub_401260以后的结果与字符串Str2一样就行了

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char Buffer[128]; // [esp+0h] [ebp-94h] BYREF
  char *Str1; // [esp+80h] [ebp-14h]
  char *Str2; // [esp+84h] [ebp-10h]
  HANDLE StdHandle; // [esp+88h] [ebp-Ch]
  HANDLE hFile; // [esp+8Ch] [ebp-8h]
  DWORD NumberOfBytesWritten; // [esp+90h] [ebp-4h] BYREF

  hFile = GetStdHandle(0xFFFFFFF5);
  StdHandle = GetStdHandle(0xFFFFFFF6);
  Str2 = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q";
  WriteFile(hFile, "Enter password:\r\n", 0x12u, &NumberOfBytesWritten, 0);
  ReadFile(StdHandle, Buffer, 0x80u, &NumberOfBytesWritten, 0);
  Str1 = sub_401260(Buffer, NumberOfBytesWritten - 2);
  if ( !strcmp(Str1, Str2) )
    WriteFile(hFile, "Correct!\r\n", 0xBu, &NumberOfBytesWritten, 0);
  else
    WriteFile(hFile, "Wrong password\r\n", 0x11u, &NumberOfBytesWritten, 0);
  return 0;
}

那就来看看sub_401260函数,起初看到一堆算法心想完蛋了,这么长的算法得逆多长时间啊,但是点进去byte_413000数组就可以发现,它就是一个变表的base64,直接写脚本或者在线自定义base64解码就行。

在这里插入图片描述

_BYTE *__cdecl sub_401260(int a1, unsigned int a2)
{
  int v3; // [esp+Ch] [ebp-24h]
  int v4; // [esp+10h] [ebp-20h]
  int v5; // [esp+14h] [ebp-1Ch]
  int i; // [esp+1Ch] [ebp-14h]
  unsigned int v7; // [esp+20h] [ebp-10h]
  _BYTE *v8; // [esp+24h] [ebp-Ch]
  int v9; // [esp+28h] [ebp-8h]
  int v10; // [esp+28h] [ebp-8h]
  unsigned int v11; // [esp+2Ch] [ebp-4h]

  v8 = malloc(4 * ((a2 + 2) / 3) + 1);
  if ( !v8 )
    return 0;
  v11 = 0;
  v9 = 0;
  while ( v11 < a2 )
  {
    v5 = *(v11 + a1);
    if ( ++v11 >= a2 )
    {
      v4 = 0;
    }
    else
    {
      v4 = *(v11 + a1);
      ++v11;
    }
    if ( v11 >= a2 )
    {
      v3 = 0;
    }
    else
    {
      v3 = *(v11 + a1);
      ++v11;
    }
    v7 = v3 + (v5 << 16) + (v4 << 8);
    v8[v9] = byte_413000[(v7 >> 18) & 0x3F];
    v10 = v9 + 1;
    v8[v10] = byte_413000[(v7 >> 12) & 0x3F];
    v8[++v10] = byte_413000[(v7 >> 6) & 0x3F];
    v8[++v10] = byte_413000[v3 & 0x3F];
    v9 = v10 + 1;
  }
  for ( i = 0; i < asc_413040[a2 % 3]; ++i )
    v8[4 * ((a2 + 2) / 3) - i - 1] = 61;
  v8[4 * ((a2 + 2) / 3)] = 0;
  return v8;
}

抱着学习的心态再写一下脚本,脚本的关键就是通过变表来还原原本正常base64表的内容,相当于重新映射一下。

import base64
base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
exbase64_table = "ZYXABCDEFGHIJKLMNOPQRSTUVWzyxabcdefghijklmnopqrstuvw0123456789+/"
cipher = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q"
original_cipher = ""
flag = ''

for i in range(len(cipher)):
    index = exbase64_table.find(cipher[i])
    original_cipher += base64_table[index]
print("flag{"+base64.b64decode(original_cipher).decode()+"}")

在这里插入图片描述

自定义base64解密

在这里插入图片描述

特殊的BASE64[base64变表]

64位exe文件,光从题目名字就已经知道这道题大概率考的是base64变表,与上面一道题一样,定位main函数看一下。

看到一串base64加密的字符串然后双击跟进以后,又发现了base64变表

在这里插入图片描述

在这里插入图片描述

所以可以写脚本了

import base64
exbase64_table = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0987654321/+"
base64_table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
cipher = "mTyqm7wjODkrNLcWl0eqO8K8gc1BPk1GNLgUpI=="
original_cipher = ""

for i in cipher:
    if i == "=":
        original_cipher += i
        continue
    index = exbase64_table.find(i)
    original_cipher += base64_table[index]
print(base64.b64decode(original_cipher).decode())

在这里插入图片描述

老样子在线网站解一下

在这里插入图片描述

[ACTF新生赛2020]Oruga[maze]

64位elf文件,拖入ida64看一下,定位main函数,逻辑很简单,输入一个字符串s,让s1的前五个字符等于输入的前五个字符,最后比较s1与s2是否一致,其实分析一下s2就是actf{,那么比较其实就是确定输入的前五个字符是actf{

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+0h] [rbp-40h]
  char s1[6]; // [rsp+4h] [rbp-3Ch] BYREF
  char s2[6]; // [rsp+Ah] [rbp-36h] BYREF
  char s[40]; // [rsp+10h] [rbp-30h] BYREF
  unsigned __int64 v8; // [rsp+38h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  memset(s, 0, 0x19uLL);
  printf("Tell me the flag:");
  scanf("%s", s);
  strcpy(s2, "actf{");
  for ( i = 0; i <= 4; ++i )
    s1[i] = s[i];
  s1[5] = 0;
  if ( !strcmp(s1, s2) )
  {
    if ( (unsigned __int8)sub_78A(s) )
      printf("That's True Flag!");
    else
      printf("don't stop trying...");
    return 0LL;
  }
  else
  {
    printf("Format false!");
    return 0LL;
  }
}

定位其中唯一一个函数sub_78A,然后我就犯难了,也没发现什么算法与自己输入做参考啊,也不像其他题那样通过逆异或得到原数据,最后看了下wp发现是个迷宫题…(其实看到那一串字符串(0占大多数)就该明白的)

_BOOL8 __fastcall sub_78A(__int64 a1)
{
  int v2; // [rsp+Ch] [rbp-Ch]
  int v3; // [rsp+10h] [rbp-8h]
  int v4; // [rsp+14h] [rbp-4h]

  v2 = 0;
  v3 = 5;
  v4 = 0;
  while ( byte_201020[v2] != '0x21' )
  {
    v2 -= v4;
    if ( *(v3 + a1) != 'W' || v4 == -16 )
    {
      if ( *(v3 + a1) != 'E' || v4 == 1 )
      {
        if ( *(v3 + a1) != 'M' || v4 == 16 )
        {
          if ( *(v3 + a1) != 'J' || v4 == -1 )
            return 0LL;
          v4 = -1;
        }
        else
        {
          v4 = 16;
        }
      }
      else
      {
        v4 = 1;
      }
    }
    else
    {
      v4 = -16;
    }
    ++v3;
    while ( !byte_201020[v2] )
    {
      if ( v4 == -1 && (v2 & 15) == 0 )
        return 0LL;
      if ( v4 == 1 && v2 % 16 == 15 )
        return 0LL;
      if ( v4 == 16 && (v2 - 240) <= 0xF )
        return 0LL;
      if ( v4 == -16 && (v2 + 15) <= 0x1E )
        return 0LL;
      v2 += v4;
    }
  }
  return *(v3 + a1) == '}';
}

所以对于迷宫题,我们就要用首先判断迷宫字符串的长度从而来确定迷宫的形状,然后再判断上下左右分别代表什么字符,最后确定终点起点即可。

因为迷宫字符串总共256个元素一看就是16*16的迷宫我们画一下迷宫。

00, 00, 00, 00, 23, 00, 00, 00, 00, 00, 00, 00, 23, 23, 23, 23,       
00, 00, 00, 23, 23, 00, 00, 00, 4F, 4F, 00, 00, 00, 00, 00, 00,       
00, 00, 00, 00, 00, 00, 00, 00, 4F, 4F, 00, 50, 50, 00, 00, 00,       
00, 00, 00, 4C, 00, 4F, 4F, 00, 4F, 4F, 00, 50, 50, 00, 00, 00,       
00, 00, 00, 4C, 00, 4F, 4F, 00, 4F, 4F, 00, 50, 00, 00, 00, 00,       
00, 00, 4C, 4C, 00, 4F, 4F, 00, 00, 00, 00, 50, 00, 00, 00, 00,       
00, 00, 00, 00, 00, 4F, 4F, 00, 00, 00, 00, 50, 00, 00, 00, 00,       
23, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00,       
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 23, 00, 00, 00,      
00, 00, 00, 00, 00, 00, 4D, 4D, 4D, 00, 00, 00, 23, 00, 00, 00,      
00, 00, 00, 00, 00, 00, 00, 4D, 4D, 4D, 00, 00, 00, 00, 45, 45,       
00, 00, 00, 30, 00, 4D, 00, 4D, 00, 4D, 00, 00, 00, 00, 45, 00,       
00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 45, 45,      
54, 54, 54, 49, 00, 4D, 00, 4D, 00, 4D, 00, 00, 00, 00, 45, 00,       
00, 54, 00, 49, 00, 4D, 00, 4D, 00, 4D, 00, 00, 00, 00, 45, 00,      
00, 54, 00, 49, 00, 4D, 00, 4D, 00, 4D, 21, 00, 00, 00, 45, 45

然后通过最外层循环while可以看出来直到0x21才停止,从而确定了终点。其实难点就是如何判断上下左右。可以看到正常在内存中的迷宫字符串是一个长度为256的一维数组,所以对其直接+1代表往右移,如果直接+16相当于直接跳到了下一行也就是向下移,同理-1-16分别代表左移上移。所以WEMJ分别代表上右下左但是看到v2的赋值方式 v2 -= v4是减着来的,这是为什么呢,这是因为这个迷宫与平常走的迷宫不一样,平常的迷宫一次只能走一步,而这个仔细看一下其实是在一个循环里面走的,因为在内层循环的最后有一句v2+=v4所以直接会一行一行走,除非碰到不是0的字符,那么此时就会在那个字符的前一步或者上一步停下,但是为了跳出循环,要求条件不满足的情况下,是多走了一步的,所以循环中第一句的v2 -= v4是为了减去多走的一步。通俗一点说就是为了跳出循环而多走的一步,需要在走的最开始减去。

所以综上这个迷宫的走法就是0可以一直走,直到碰到其他障碍,然后终点是21。自己画一下就行,还是比较简单的。

在这里插入图片描述

最终flag就是actf{MEWEMEWJMEWJM}

[ACTF新生赛2020]Universe_final_answer[z3]

64位elf文件,拖入IDA64中定位main函数

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v4; // [rsp+0h] [rbp-A8h] BYREF
  char v5[104]; // [rsp+20h] [rbp-88h] BYREF
  unsigned __int64 v6; // [rsp+88h] [rbp-20h]

  v6 = __readfsqword(0x28u);
  __printf_chk(1LL, "Please give me the key string:", a3);
  scanf("%s", v5);
  if ( sub_860(v5) )
  {
    sub_C50(v5, &v4);
    __printf_chk(1LL, "Judgement pass! flag is actf{%s_%s}\n", v5);
  }
  else
  {
    puts("False key!");
  }
  return 0LL;
}

可以看到有两个函数都对我们的输入v5进行了处理,所以我们再跟进一下

bool __fastcall sub_860(char *a1)
{
  int v1; // ecx
  int v2; // esi
  int v3; // edx
  int v4; // r9d
  int v5; // r11d
  int v6; // ebp
  int v7; // ebx
  int v8; // r8d
  int v9; // r10d
  bool result; // al
  int v11; // [rsp+0h] [rbp-38h]

  v1 = a1[1];
  v2 = *a1;
  v3 = a1[2];
  v4 = a1[3];
  v5 = a1[4];
  v6 = a1[6];
  v7 = a1[5];
  v8 = a1[7];
  v9 = a1[8];
  result = 0;
  if ( -85 * v9 + 58 * v8 + 97 * v6 + v7 + -45 * v5 + 84 * v4 + 95 * v2 - 20 * v1 + 12 * v3 == 0x3145 )
  {
    v11 = a1[9];
    if ( 30 * v11 + -70 * v9 + -122 * v6 + -81 * v7 + -66 * v5 + -115 * v4 + -41 * v3 + -86 * v1 - 15 * v2 - 30 * v8 == 0xFFFF2B80
      && -103 * v11 + 120 * v8 + 108 * v7 + 48 * v4 + -89 * v3 + 78 * v1 - 41 * v2 + 31 * v5 - (v6 << 6) - 120 * v9 == 0xFFFFD7D5
      && 71 * v6 + (v7 << 7) + 99 * v5 + -111 * v3 + 85 * v1 + 79 * v2 - 30 * v4 - 119 * v8 + 48 * v9 - 16 * v11 == 0x5947
      && 5 * v11 + 23 * v9 + 122 * v8 + -19 * v6 + 99 * v7 + -117 * v5 + -69 * v3 + 22 * v1 - 98 * v2 + 10 * v4 == 0xFFFFF480
      && -54 * v11 + -23 * v8 + -82 * v3 + -85 * v2 + 124 * v1 - 11 * v4 - 8 * v5 - 60 * v7 + 95 * v6 + 100 * v9 == 0xFFFFF752
      && -83 * v11 + -111 * v7 + -57 * v2 + 41 * v1 + 73 * v3 - 18 * v4 + 26 * v5 + 16 * v6 + 77 * v8 - 63 * v9 == 0xFFFFCC36
      && 81 * v11 + -48 * v9 + 66 * v8 + -104 * v6 + -121 * v7 + 95 * v5 + 85 * v4 + 60 * v3 + -85 * v2 + 80 * v1 == 0xFFFFF9E9
      && 101 * v11 + -85 * v9 + 7 * v6 + 117 * v7 + -83 * v5 + -101 * v4 + 90 * v3 + -28 * v1 + 18 * v2 - v8 == 0x18A4 )
    {
      return 99 * v11 + -28 * v9 + 5 * v8 + 93 * v6 + -18 * v7 + -127 * v5 + 6 * v4 + -9 * v3 + -93 * v1 + 58 * v2 == 0xFFFFF95F;
    }
  }
  return result;
}

看到这个函数里面的内容,应该可以想到要z3来解方程先算一下v1-v11是多少,最后在把其与a1对应即可

from z3 import *

z = Solver()
v1 = Int('v1')
v2 = Int('v2')
v3 = Int('v3')
v4 = Int('v4')
v5 = Int('v5')
v6 = Int('v6')
v7 = Int('v7')
v8 = Int('v8')
v9 = Int('v9')
v11 = Int('v11')
z.add(-85 * v9 + 58 * v8 + 97 * v6 + v7 + -45 * v5 + 84 * v4 + 95 * v2 - 20 * v1 + 12 * v3 == 12613)
z.add(30 * v11 + -70 * v9 + -122 * v6 + -81 * v7 + -66 * v5 + -115 * v4 + -41 * v3 + -86 * v1 - 15 * v2 - 30 * v8 == -54400)
z.add(-103 * v11 + 120 * v8 + 108 * v7 + 48 * v4 + -89 * v3 + 78 * v1 - 41 * v2 + 31 * v5 - (v6*64) - 120 * v9 == -10283) #这里要把左移6位改为乘以64
z.add(71 * v6 + (v7 * 128) + 99 * v5 + -111 * v3 + 85 * v1 + 79 * v2 - 30 * v4 - 119 * v8 + 48 * v9 - 16 * v11 == 22855)#这里要把左移7位改为乘以128
z.add(5 * v11 + 23 * v9 + 122 * v8 + -19 * v6 + 99 * v7 + -117 * v5 + -69 * v3 + 22 * v1 - 98 * v2 + 10 * v4 == -2944)
z.add(-54 * v11 + -23 * v8 + -82 * v3 + -85 * v2 + 124 * v1 - 11 * v4 - 8 * v5 - 60 * v7 + 95 * v6 + 100 * v9 == -2222)
z.add(-83 * v11 + -111 * v7 + -57 * v2 + 41 * v1 + 73 * v3 - 18 * v4 + 26 * v5 + 16 * v6 + 77 * v8 - 63 * v9 == -13258)
z.add(81 * v11 + -48 * v9 + 66 * v8 + -104 * v6 + -121 * v7 + 95 * v5 + 85 * v4 + 60 * v3 + -85 * v2 + 80 * v1 == -1559)
z.add(101 * v11 + -85 * v9 + 7 * v6 + 117 * v7 + -83 * v5 + -101 * v4 + 90 * v3 + -28 * v1 + 18 * v2 - v8 == 6308)
z.add(99 * v11 + -28 * v9 + 5 * v8 + 93 * v6 + -18 * v7 + -127 * v5 + 6 * v4 + -9 * v3 + -93 * v1 + 58 * v2 == -1697)

z.check()
print(z.model())

在这里插入图片描述

然后根据数组a1的顺序重新排一下顺序

a1 = [0] * 10
a1[1] = 48
a1[6] = 95
a1[0] = 70
a1[3] = 82
a1[9] = 64
a1[2] = 117
a1[4] = 84
a1[8] = 119
a1[7] = 55
a1[5] = 121
res = []
for i in range(10):
    res.append(a1[i])
print(res)
#[70, 48, 117, 82, 84, 121, 95, 55, 119, 64]
key = ''
for i in range(len(res)):
    key += chr(res[i])
print(key)
#F0uRTy_7w@

得到了密钥以后,继续往下分析,其实可以确定的是函数sub_C50是通过我们生成的密钥,来得到最后的flag的,所以只要把密钥输出到程序中即可

在这里插入图片描述

[BJDCTF2020]BJD hamburger competition[Unity逆向、C#逆向]

下载附件以后发现是一个64位的程序是用Unity做的,打开看看发现是老八秘制小汉堡…

在这里插入图片描述

拖入IDA看看吧,但是找了半天就只找到了一个WinMain函数,也没有找到有用的信息

// attributes: thunk
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
  return UnityMain(hInstance, hPrevInstance, lpCmdLine, nShowCmd);
}

最后查了一下wp发现是一个关于Unity的逆向题

对于Windows平台打包生成的.exe文件,Unity会将你的代码和相关资源打包到一个单独的数据文件夹(通常命名为"_Data"或类似名称)中。这个数据文件夹与.exe文件位于同一目录下。在这个数据文件夹内,你的代码会被编译成动态链接库(DLL)文件,通常命名为"Assembly-CSharp.dll"或类似名称。这个DLL文件包含了你的项目中所有的C#代码。一般C#的反汇编工具为dnspy或者是ILSpy

在这里插入图片描述

dnspy下载地址[https://github.com/dnSpy/dnSpy/releases]

在这里插入图片描述

下载解压完以后,将其放入对应的x86/x64程序即可,然后打开Assembly-Csharp.dll文件,因为这是我们所有代码的集合

在这里插入图片描述

然后玩过游戏以后发现这些菜叶汉堡片什么的都可以点的,那么就从一堆模块中选中了ButtonSpawnFruit,然后定位到了关键代码

public void Spawn()
	{
		FruitSpawner component = GameObject.FindWithTag("GameController").GetComponent<FruitSpawner>();
		if (component)
		{
			if (this.audioSources.Length != 0)
			{
				this.audioSources[Random.Range(0, this.audioSources.Length)].Play();
			}
			component.Spawn(this.toSpawn);
			string name = this.toSpawn.name;
			if (name == "汉堡底" && Init.spawnCount == 0)
			{
				Init.secret += 997;
			}
			else if (name == "鸭屁股")
			{
				Init.secret -= 127;
			}
			else if (name == "胡罗贝")
			{
				Init.secret *= 3;
			}
			else if (name == "臭豆腐")
			{
				Init.secret ^= 18;
			}
			else if (name == "俘虏")
			{
				Init.secret += 29;
			}
			else if (name == "白拆")
			{
				Init.secret -= 47;
			}
			else if (name == "美汁汁")
			{
				Init.secret *= 5;
			}
			else if (name == "柠檬")
			{
				Init.secret ^= 87;
			}
			else if (name == "汉堡顶" && Init.spawnCount == 5)
			{
				Init.secret ^= 127;
				string str = Init.secret.ToString();
				if (ButtonSpawnFruit.Sha1(str) == "DD01903921EA24941C26A48F2CEC24E0BB0E8CC7")
				{
					this.result = "BJDCTF{" + ButtonSpawnFruit.Md5(str) + "}";
					Debug.Log(this.result);
				}
			}
			Init.spawnCount++;
			Debug.Log(Init.secret);
			Debug.Log(Init.spawnCount);
		}
	}

可以看到代码的最后几句有一个if语句,如果str经过sha1加密以后为DD01903921EA24941C26A48F2CEC24E0BB0E8CC7,那么就会输出flag,先在线解密一下

在这里插入图片描述

明文是1001,此时再定位Md5函数,最后的flag是将1001Md5以后的结果

public static string Md5(string str)
{
	byte[] bytes = Encoding.UTF8.GetBytes(str);
	byte[] array = MD5.Create().ComputeHash(bytes);
	StringBuilder stringBuilder = new StringBuilder();
	foreach (byte b in array)
	{
		stringBuilder.Append(b.ToString("X2"));
	}
	return stringBuilder.ToString().Substring(0, 20);
}
//可以看到这个代码最后是截取了前20位做结果的
//b8c37e33defde51cf91e1e03e51657da

在C#中,ToString("X2") 是一个格式化字符串的方法,用于将数字转换为十六进制(Hexadecimal)表示,并确保结果至少有两位字符。如果原始数字的十六进制表示少于两位,它会在前面填充零以达到两位长度。且转换后的结果是大写字母

string = "b8c37e33defde51cf91e1e03e51657da"
res = string.swapcase()
print("flag{"+ res[0:20]+"}")

如果用ILSpy的话需要下载安装就比较麻烦,第一次使用的时候记得要从GAC导入,否则好多模块无法识别。

下载地址https://github.com/icsharpcode/ILSpy/releases

打开同样是这个界面

在这里插入图片描述

从这道题还是学到了很多的知识,包括对Unity、C#的逆向

[Zer0pts2020]easy strcmp[算法分析、init段、hook]

64elf文件,拖入IDA看看, 定位main函数

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  if ( a1 > 1 )
  {
    if ( !strcmp(a2[1], "zer0pts{********CENSORED********}") )
      puts("Correct!");
    else
      puts("Wrong!");
  }
  else
  {
    printf("Usage: %s <FLAG>\n", *a2);
  }
  return 0LL;
}

然后定位几个关键的函数,点开可以发现其中sub_6E0、sub_650函数没有实际用处

在这里插入图片描述

其中sub_610的函数只是返回一个数组,也没有实际用处。接下来定位一下sub_6EA

__int64 __fastcall sub_6EA(__int64 a1, __int64 a2)
{
  int i; // [rsp+18h] [rbp-8h]
  int v4; // [rsp+18h] [rbp-8h]
  int j; // [rsp+1Ch] [rbp-4h]

  for ( i = 0; *(i + a1); ++i )
    ;
  v4 = (i >> 3) + 1;
  for ( j = 0; j < v4; ++j )
    *(8 * j + a1) -= qword_201060[j];
  return qword_201090(a1, a2);
}

实际上,我们看一下init段

在这里插入图片描述

可以看到分别调用了6E0与795函数

在这里插入图片描述

其中qword_201090的函数会跳转到strcmp,然后sub_6EA的地址又会返回到off_201028里面

在这里插入图片描述

但其实off_201028实际上是strcmp的地址

在这里插入图片描述

所以综合下来就是利用hook的方式将我们的strcmp函数的地址改写了sub_6EA,也就是执行strcmp实际上就是执行函数sub_ 6EA,所以就可以写脚本了

qword_201060=[0x42, 0x09, 0x4A, 0x49, 0x35, 0x43, 0x0A, 0x41, 0xF0, 0x19, 0xE6, 0x0B, 0xF5, 0xF2, 0x0E, 0x0B, 0x2B, 0x28, 0x35, 0x4A, 0x06, 0x3A, 0x0A, 0x4F] #转成小端序后
a1 = "********CENSORED********"
flag =''
for i in range(len(qword_201060)):
    res = qword_201060[i] + ord(a1[i])
    if res > 127:
        res %= 128
        flag += chr(res)
    else:
        flag += chr(res)
print(flag)

在这里插入图片描述

其中发现一个脚本的bug,因为有的数太大的话加起来已经超过了127也就超过了ASCII值的范围,所以要模一下128,但是如果我分块一个字节一个字节的分开运算的话,其中有两个下划线总会无法生成以及有几个字母无法与flag一致。所以用了下网上的脚本

import binascii
str1 = '********'
str2 = 'CENSORED'
str3 = '********'
key = [0x42094A4935430A41, 0x0B0EF2F50BE619F0, 0x2B28354A063A0A4F]
bin1 = binascii.b2a_hex(str1.encode('ascii'))
bin2 = binascii.b2a_hex(str2.encode('ascii')[::-1])
bin3 = binascii.b2a_hex(str3.encode('ascii'))
#print(bin1,bin2,bin3)
res1 = binascii.a2b_hex(hex(int(bin1,16) + key[0])[2:])
res2 = binascii.a2b_hex(hex(int(bin2,16) + key[1])[2:])[::-1] #这个有点问题就是如果直接将其改为小端序会溢出所以就会导致这个函数无法转换成正确的字符串,所以还是用正常顺序,最后倒序输出结果吧
res3 = binascii.a2b_hex(hex(int(bin3,16) + key[2])[2:])
print("flag{"+(res1+res2+res3).decode()+"}")

在这里插入图片描述

写到这里才知道,其实这也就是上面我那个脚本的问题,没有考虑到进位,所以那个下划线始终出不来,所以还是要整体进行加减法,免得丢失精度。

通过这道题我学到了,进行大整数加减法转换为十六进制要注意进位与溢出,以及用hook方式更改函数内容

[WUSTCTF2020]level4[DS、二叉树遍历]

64位elf文件,定位到main函数发现是一个闯关游戏,先运行一下发现输出的是一堆乱码

在这里插入图片描述

int __cdecl main(int argc, const char **argv, const char **envp)
{
  puts("Practice my Data Structure code.....");
  puts("Typing....Struct.....char....*left....*right............emmmmm...OK!");
  init();
  puts("Traversal!");
  printf("Traversal type 1:");
  type1(&qword_601290);
  printf("\nTraversal type 2:");
  type2(&qword_601290);
  printf("\nTraversal type 3:");
  puts("    //type3(&x[22]);   No way!");
  puts(&byte_400A37);
  return 0;
}

其中我们定位一下init()函数,其中里面有一句赋值语句,这应该就是这道题全部的字符串了

strcpy(v2, "I{_}Af2700ih_secTS2Et_wr");

然后结合题目的提示这是一个数据结构,然后还有*left*right所以就会想到二叉树中,左子树和右子树都是用这种指针的方式表达的,然后再结合输出的两个乱码以及type1和type2可以看出分别是

前序遍历:根 -> 左 -> 右

中序遍历:左 -> 根 -> 右

后序遍历:左 -> 右 -> 根

__int64 __fastcall type1(char **a1)
{
  __int64 result; // rax

  if ( a1 )
  {
    type1(a1[1]);
    putchar(*a1);
    return type1(a1[2]); 
  }
  return result;
}//可以看到先递归的根节点,那么毫无疑问是中序遍历
int __fastcall type2(char *a1)
{
  int result; // eax

  if ( a1 )
  {
    type2(*(a1 + 1));
    type2(*(a1 + 2));
    return putchar(*a1);
  }
  return result;
}//可以看出这个是后序遍历,因为这个递归最开始是从左子树,然后就是右子树递归,最后返回根节点

所以这道题实际上成了考我们, 已知一个二叉树的中序遍历与后序遍历,求其前序遍历,也就是正确的flag

中序遍历:2f0t02T{hcsiI_SwA__r7Ee}

后序遍历:20f0Th{2tsIS_icArE}e7__w

自己画了个图解出来,就按照以前数据结构学过的已知中序、后序遍历求整个二叉树的方法,通过后序遍历求树的根节点,再通过中序遍历求树的左右子树,以此类推一点一点推即可得到整个树,最后再用前序遍历读出来即可

在这里插入图片描述

所以最后flag为wctf2020{This_IS_A_7reE}

后面试着自己写代码来跑一下,自己这么算太难了。其实这道题并不难,难的是能知道考的是二叉树的遍历,我其实一看到这两个type函数以及init函数中一堆赋值(实际上是对树节点的赋值)就怕了,其实结合它题目的提示,以及代码大概就能分辨出来,但其实还是自己的数据结构不太好,只会写题。。。但是对于代码就不太敏感

[羊城杯 2020]easyre[算法逆向、Caesar]

64位exe,拖入ida看看,定位到main函数

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  int v4; // eax
  int v5; // eax
  char Str[48]; // [rsp+20h] [rbp-60h] BYREF
  char Str1[64]; // [rsp+50h] [rbp-30h] BYREF
  char v9[64]; // [rsp+90h] [rbp+10h] BYREF
  char v10[64]; // [rsp+D0h] [rbp+50h] BYREF
  char Str2[60]; // [rsp+110h] [rbp+90h] BYREF
  int v12; // [rsp+14Ch] [rbp+CCh] BYREF

  _main();
  strcpy(Str2, "EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG");
  puts("Hello, please input your flag and I will tell you whether it is right or not.");
  scanf("%38s", Str);
  if ( strlen(Str) != 38 //这里确定Str的长度就为38
    || (v3 = strlen(Str), (unsigned int)encode_one(Str, v3, v10, &v12)) //v3=38
    || (v4 = strlen(v10), (unsigned int)encode_two(v10, v4, v9, &v12))
    || (v5 = strlen(v9), (unsigned int)encode_three(v9, v5, Str1, &v12))
    || strcmp(Str1, Str2) )
  {
    printf("Something wrong. Keep going.");
    return 0;
  }
  else
  {
    puts("you are right!");
    return 0;
  }
}

其中比较关键的函数就是encode_onetwothree了,通过findcrypt插件或者看变量的内容可以得知encode_one是简单的base64,所以v10就是就是最终由输入的内容经过一次base64加密的内容

在这里插入图片描述

接下来看一下encode_two

__int64 __fastcall encode_two(const char *a1, int a2, char *a3, int *a4)
{
  if ( !a1 || !a2 )
    return 0xFFFFFFFF;
  strncpy(a3, a1 + 26, 13u);
  strncpy(a3 + 13, a1, 13u);
  strncpy(a3 + 26, a1 + 39, 13u);
  strncpy(a3 + 39, a1 + 13, 13u);
  return 0i64;
}

这个代码相当于将刚才经过base64加密的部分更改了对应的顺序,将其按照13个字符分为4组,对应的下标分别为, 0-12(1)、13-25(2)、26-38(3)、39-51(4), 更改完顺序以后a3的内容就变为了3、1、4、2部分。

最后看一下encode_three部分也是一个简单的加密

__int64 __fastcall encode_three(const char *a1, int a2, char *a3, int *a4)
{
  char v5; // [rsp+Fh] [rbp-11h]
  int i; // [rsp+14h] [rbp-Ch]
  const char *v8; // [rsp+30h] [rbp+10h]

  v8 = a1; //v8指向了a1数组也就是刚才的v9
  if ( !a1 || !a2 )
    return 0xFFFFFFFF;
  for ( i = 0; i < a2; ++i )
  {
    v5 = *v8; //v5 = v8[0] = v9[0]
    if ( *v8 <= 64 || v5 > 90 ) //这里的条件其实都是指的是v9[0] 
    {
      if ( v5 <= 96 || v5 > 122 )
      {
        if ( v5 <= 47 || v5 > 57 )
          *a3 = v5;  //a3[0] = v9[0]
        else
          *a3 = (v5 - 48 + 3) % 10 + 48; // (str1[i] - 48) %26 + 45
      }
      else
      {
        *a3 = (v5 - 97 + 3) % 26 + 97; //(str1[i] - 97) % 26 + 94
      }
    }
    else
    {
      *a3 = (v5 - 65 + 3) % 26 + 65; // (str1[i] - 65) % 26 + 62
    }
    ++a3;
    ++v8;
  }
  return 0;
}

这里的a1就是上次分组的v9,a3就是最后生成的字符串str1,加密后最后返回的字符串要与题目所给的str1一样也就是

EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG 这个加密代码只会对数字、大写字母、小写字母进行加密,所以写个脚本一步一步还原即可

直接暴力破解encode_two即可

import base64
str1 = "EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG"
v9 = ''
v10 = ''
flag = ''
for j in str1:
    for i in range(32,127):
        if i <= 64 or i > 90:
            if i <= 96 or  i > 122:
                if i <= 47 or i > 57:
                    tmp = chr(i)
                else:
                    tmp = chr( ( i - 48 + 3 ) % 10 + ord('0') )
            else:
                tmp = chr( (i - 97 + 3) % 26 + ord('a') )     
        else:
            tmp = chr( (i - 65 + 3) % 26 + ord('A') )
        if tmp == j:
            v9 += chr(i)
            break
#print(v9)
# BjYjM2Mjk4NzMR1dIVHs2NzJjY0MTEzM2VhMn0=zQ3NzhhMzhlOD
v10 = v9[13:26] + v9[39:] + v9[:13] + v9[26:39] 
#print(v10)
# R1dIVHs2NzJjYzQ3NzhhMzhlODBjYjM2Mjk4NzM0MTEzM2VhMn0=
flag = base64.b64decode(v10).decode()
print(flag)

在这里插入图片描述

但是其实encode_two只是简单的凯撒密码其中key=3,也就是让某个字符循环右移3位,比如小写字母、大写字母、数字,所以解密操作也比较简单,就是在原基础上减去key即可

cipher = "EmBmP5Pmn7QcPU4gLYKv5QcMmB3PWHcP5YkPq3=cT6QckkPckoRG" #经过凯撒密码加密后的内容
text = ''
for i in cipher:
    tmp = ord(i)
    if 48 <= tmp <= 57: #数字0-9
        text  += chr(( tmp - ord('0') - 3 ) % 10 + ord('0'))   #其中减48相当于求出这个字符在0-9中的位置,然后左移3个(加密操作的逆操作),最后再加上0的ascii码,将其转换到0-9的ascii码范围
    elif 97 <= tmp <= 122: #小写字母a-z
        text += chr(( tmp -  ord('a') - 3 ) % 26 +  ord('a'))
    elif 65 <= tmp <= 90: #大写字母A-Z
        text += chr(( tmp - ord('A') - 3 ) % 26 +  ord('A'))
	else:
        text += chr(tmp)
print(text)
#BjYjM2Mjk4NzMR1dIVHs2NzJjY0MTEzM2VhMn0=zQ3NzhhMzhlOD

在这里插入图片描述

[网鼎杯 2020 青龙组]singal[算法分析,虚拟机指令、angr]

正常解法:

32位exe,拖入ida看看,定位main函数发现首先会把unk_403040数组中前456个字节赋值到v4中,然后就会经过vm_operad函数,至于为什么是456,因为函数名称是qmemcpy是四字节复制函数,所以对于114个四字节总共就是456个字节。

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4[117]; // [esp+18h] [ebp-1D4h] BYREF

  __main();
  qmemcpy(v4, &unk_403040, 0x1C8u);
  vm_operad(v4, 114);
  puts("good,The answer format is:flag {}");
  return 0;
}

首先通过ida自带的python来提取一下数组unk_403040的内容

base = 0x00403040
end = 0x00403205
res = []

for i in range(base,end,4):
    tmp = get_wide_dword(i)
    res.append(tmp)
print(res)
a1 = [10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1, 7, 34, 7, 63, 7, 52, 7, 50, 7, 114, 7, 51, 7, 24, 7, 4294967207, 7, 49, 7, 4294967281, 7, 40, 7, 4294967172, 7, 4294967233, 7, 30, 7, 122]

其中有几个特别的大数是因为获取的dword所以自动把后面的FF FF FF加上了

在这里插入图片描述

在这里插入图片描述

所以更改一下结果

a1=[10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1, 7, 34, 7, 63, 7, 52, 7, 50, 7, 114, 7, 51, 7, 24, 7, 167, 7, 49, 7, 241, 7, 40, 7, 132, 7, 193, 7, 30, 7, 122]

我们再定位一下vm_operad函数,发现是一个switch case语句,发现会根据a1里面的具体元素来确定到底执行什么语句

然后当case=10的情况下,会有一个read函数,会输入一个长度为15的字符串

size_t __cdecl read(char *Str)
{
  size_t result; // eax

  printf("string:");
  scanf("%s", Str);
  result = strlen(Str);
  if ( result != 15 )
  {
    puts("WRONG!\n");
    exit(0);
  }
  return result;
}

可以试着走一下这个循环,比如在最开始的循环中,v9=0,此时a1[v9] = 10就会触发case10的情况,就会提示输入一个长度为15的字符串Str,然后v9=1,此时a1[v9] = 4,此时v4会被赋值,然后指针向下指两个字符反复循环

int __cdecl vm_operad(int *a1, int a2)
{
  int result; // eax
  char Str[200]; // [esp+13h] [ebp-E5h] BYREF
  char v4; // [esp+DBh] [ebp-1Dh]
  int v5; // [esp+DCh] [ebp-1Ch]
  int v6; // [esp+E0h] [ebp-18h]
  int v7; // [esp+E4h] [ebp-14h]
  int v8; // [esp+E8h] [ebp-10h]
  int v9; // [esp+ECh] [ebp-Ch]

  v9 = 0;
  v8 = 0;
  v7 = 0;
  v6 = 0;
  v5 = 0;
  while ( 1 )
  {
    result = v9;
    if ( v9 >= a2 )
      return result;
    switch ( a1[v9] )
    {
      case 1:
        Str[v6 + 100] = v4; //+100的原因就是前面的操作指令只有100个剩下的全是需要比较的
        ++v9;
        ++v6;
        ++v8;
        break;
      case 2:
        v4 = a1[v9 + 1] + Str[v8];
        v9 += 2;
        break;
      case 3:
        v4 = Str[v8] - LOBYTE(a1[v9 + 1]);
        v9 += 2;
        break;
      case 4:
        v4 = a1[v9 + 1] ^ Str[v8];
        v9 += 2;
        break;
      case 5:
        v4 = a1[v9 + 1] * Str[v8];
        v9 += 2;
        break;
      case 6:
        ++v9;
        break;
      case 7:
        if ( Str[v7 + 100] != a1[v9 + 1] ) //v7这里是第一次出现,以后每当遇到7就会与7的下一个字符进行比较
        {
          printf("what a shame...");
          exit(0);
        }
        ++v7;
        v9 += 2;
        break;
      case 8:
        Str[v5] = v4;
        ++v9;
        ++v5;
        break;
      case 10:
        read(Str);
        ++v9;
        break;
      case 11:
        v4 = Str[v8] - 1;
        ++v9;
        break;
      case 12:
        v4 = Str[v8] + 1;
        ++v9;
        break;
      default:
        continue;
    }
  }
}

我们可以先提取一下用来比较的数据,也就是7后面的数据,这些数据就是最后用来比较的

a1 = [10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1, 7, 34, 7, 63, 7, 52, 7, 50, 7, 114, 7, 51, 7, 24, 7, 167, 7, 49, 7, 241, 7, 40, 7, 132, 7, 193, 7, 30, 7, 122]
res = []
for i in range(len(a1)):
    if a1[i] == 7:
        res.append(a1[i+1])
print(res)
# res = [34, 63, 52, 50, 114, 51, 24, 167, 49, 241, 40, 132, 193, 30, 122] 总共15个

当case = 7的时候会进行一个比较,Str[v7+100](实际上就是从第100个因为v7只有当case7才会变化)与a1中等于7的下一个字符相等就继续,不相等就退出了。所以这也提示我们从Str[100]开始往后15个字符就是刚才得到的比较数据

然后继续注意到只有当case1的时候,v4会赋值给Str[v6 + 100](其实也一样因为v6只有在case1才变化)这也是Str[100]以后那15个字符的唯一获取方式。

再看下其他指令,发现是是分别对v4进行赋值的,是用指令与Str的前面的字节进行一定的运算得到的,又因为,实际上v4str[v6+100](关键数据)一致,那么就能通过逆推来得到Str前面的字符也就是flag了,又因为case10的提示,所以flag是Str的前15个字节。所以代码里面的Str[v5]、Str[v8]都相当于flag

所以关键内容就是case1里面生成的也就是我们的关键数据,有了这些数据才能反推得到flag。首先我们正常运行一下程序求一下指令的执行顺序(因为遇到不同的case,v9自增自减的幅度也不同)

#include <stdio.h>
#include <string.h>
 
unsigned int vmcode[] =  //虚拟机指令,去掉了多余的0
{
	10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1, 7, 34, 7, 63, 7, 52, 7, 50, 7, 114, 7, 51, 7, 24, 7, 167, 7, 49, 7, 241, 7, 40, 7, 132, 7, 193, 7, 30, 7, 122 
};

int *a = vmcode;
int vm_operad(int *opcode, int len)
{
        char order[114] = {};        //指令执行的顺序        
        char flag[100]; //代表Str[0-14]
        char v4[100];  //代表Str[100-1xx]
        char v5; 
        int j;  //代表v5
        int m;  //代表v6
        int k;  //代表v7
        int n; //代表v8
        int i; //代表v9
        int s = 0; //代表存放执行顺序数组的索引
 	    int ss = 0; 
        i = 0;
        n = 0;
        k = 0;
        m = 0;
        j = 0;
        while (1)
        {
                if (i >= len_114)     // 超过指令长度就退出
                        break;
                switch (opcode[i])
                {
                case 1:
                        v4[m] = v5;
                        ++i;
                        ++m;
                        ++n;
                        break;
                case 2:
                        v5 = opcode[i + 1] + flag[n];
                        i += 2;
                        break;
                case 3:
                        v5 = flag[n] - opcode[i + 1];
                        i += 2;
                        break;
                case 4:
                        v5 = opcode[i + 1] ^ flag[n];
                        i += 2;
                        break;
                case 5:
                        v5 = opcode[i + 1] * flag[n];
                        i += 2;
                        break;
                case 6:
                        ++i;
                        break;
                case 7:
                        v4[k] = opcode[i + 1];           // 打印V4其实也是我们求7后面的数据共15个
                        printf("%#X, ", v4[k]);                
                        ++k;
                        i += 2;
                        break;
                case 8:
                        flag[j] = v5;
                        ++i;
                        ++j;
                        break;
                case 10:
                        printf("Please input flag:\n");
                        scanf("%s", flag);               // 输入flag          
                        ++i;
                        break;
                case 11:
                        v5 = flag[n] - 1;
                        ++i;
                        break;
                case 12:
                        v5 = flag[n] + 1;
                        ++i;
                        break;
                }
                order[s++] = i;
        }
        printf("执行顺序是: ");
        for (ss = 0; ss < strlen(order); ss++) {
                printf("%d, ", order[ss]);
        }
        return 0;
}
 
int main()
{        
        vm_operad(a, 114);   
        return 0;
}

在这里插入图片描述

有了指令的执行顺序就可以求出flag了

#include<stdio.h>
#include<string.h>
int main()
{
        int i;
        //获取flag的关键数据
        int v4[] = { 34, 63, 52, 50, 114, 51, 24, 167, 49, 241, 40, 132, 193, 30, 122 };
        //执行虚拟机指令的索引,即执行顺序
        char order[100] = { 1, 3, 4, 6, 7, 9, 10, 12, 13, 15, 16, 17, 18, 19, 20, 22, 23, 25, 26, 28, 29, 30, 31, 32, 33, 35, 36, 38, 39, 41, 42, 44, 45, 46, 47, 48, 49, 51, 52, 54, 55, 57, 58, 60, 61, 63, 64, 66, 67, 69, 70, 72, 73, 75, 76, 78, 79, 81, 82, 83, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114 };
    	int opcode[] = {10, 4, 16, 8, 3, 5, 1, 4, 32, 8, 5, 3, 1, 3, 2, 8, 11, 1, 12, 8, 4, 4, 1, 5, 3, 8, 3, 33, 1, 11, 8, 11, 1, 4, 9, 8, 3, 32, 1, 2, 81, 8, 4, 36, 1, 12, 8, 11, 1, 5, 2, 8, 2, 37, 1, 2, 54, 8, 4, 65, 1, 2, 32, 8, 5, 1, 1, 5, 3, 8, 2, 37, 1, 4, 9, 8, 3, 32, 1, 2, 65, 8, 12, 1, 7, 34, 7, 63, 7, 52, 7, 50, 7, 114, 7, 51, 7, 24, 7, 167, 7, 49, 7, 241, 7, 40, 7, 132, 7, 193, 7, 30, 7, 122 };
        unsigned char flag[100] = {};
        int m = 15; //因为flag只有15位
        int n = 15;
        int j = 15;
        int v5;
    	int k;
        for (k = strlen(order) - 1; k >= 0 ; k--)
        {
                i = order[k];
                switch (opcode[i])         // 倒序执行
                {
                case 1:
                        --m;
                        --n;
                        v5 = v4[m];
                        break;
                case 2:
                        flag[n] = (char)(v5 - opcode[i + 1]); 
                        break;
                case 3:
                    	flag[n] = (char)(v5 + opcode[i + 1]);
                        break;
                case 4:
                        flag[n] = (char)(v5 ^ opcode[i + 1]);
                        break;
                case 5:
                        flag[n] = (char)(v5 / opcode[i + 1]);
                        break;
                case 6:
                        break;
                case 8:
                        v5 = flag[--j];
                        break;
                case 11:
                        flag[n] = (char)(v5 + 1);
                        break;
                case 12:
                        flag[n] = (char)(v5 - 1);
                        break;
                }
        }
        printf("%s", flag);
        return 0;
}

在这里插入图片描述

利用Python angr库

angr是一个用于二进制分析的Python框架,它提供了符号执行、静态分析、模糊测试等工具和功能。通过使用angr,用户可以更方便地处理和分析二进制文件,以发现其中的漏洞和安全问题。

首先安装angr库

pip install angr

这里推荐用Linux环境且用python的虚拟环境,因为Windows下面会出现bug,避免出现依赖重复

其中就是报了各种没有此模块的错误,于是上网查了一下,发现用虚拟环境即可避免这些问题

Kali Linux安装虚拟环境

apt install python3.11-venv
python -m venv mynewvenv #进入项目文件夹
source mynewvenv/bin/activate #进入虚拟环境
pip -list #查看安装了的库
#这样安装的库就不会与实际重复了
import angr

p = angr.Project('./signal.exe')   #指定angr跑的程序
state = p.factory.entry_state()    #新建一个SimState的对象,得到一个初始化到二进制入口函数的SimState对象。
simgr = p.factory.simgr(state)   #创建simulation manager,angr的主要入口

simgr.explore(find=0x004017A5 ,avoid=0x004016E6)  #争取跑到输出成功的地址,避免跑到输出wrong的地址
flag = simgr.found[0].posix.dumps(0)[:15]     #得到flag
print(flag)

因为可以确定正确与错误输出的语句的地址,所以在两个地址上面就填这两个地址即可

在这里插入图片描述

在这里插入图片描述

运行结果如下

在这里插入图片描述

reference:http://120.24.80.93/index.php/2021/12/23/wangdingcup_2020_singal/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/477816.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

科技强国:国产桌面操作系统正式上线,中国七位院士发表支持

操作系统是电子产品的核心&#xff0c;无论是手机还是电脑&#xff0c;都离不开它的支持。它是科技发展中至关重要的领域之一。备受瞩目的鸿蒙系统&#xff0c;作为国内颇具影响力的手机桌面操作系统&#xff0c;以其桌面组件自由整合和自定义风格的便捷性&#xff0c;引领了一…

运用YOLOv5实时监测并预警行人社交距离违规情况

YOLO&#xff08;You Only Look Once&#xff09;作为一种先进的实时物体检测算法&#xff0c;在全球范围内因其高效的实时性能和较高的检测精度受到广泛关注。近年来&#xff0c;随着新冠疫情对社交距离管控的重要性日益凸显&#xff0c;研究人员开始将YOLO算法应用于社交距离…

5G安全技术新突破!亚信安全5G安全迅龙引擎正式发布

5G专网应用飞速增长&#xff1a;2020年5G专网数量800个&#xff0c;2021年2300个&#xff0c;2022年5325个&#xff0c;2023年已经超过16000个&#xff0c;5G与垂直行业的融合快速加深&#xff0c;5G带来的变革正加速渗透至各行各业。 5G网络出现安全问题&#xff0c;将是异常严…

QT tableWidget横向众向设置

横向控件 要设置QTabWidget选项卡的字体方向&#xff0c;可以使用QTabWidget的setTabPosition()方法。通过传递Qt枚举值QTabWidget.east或QTabWidget.west作为参数&#xff0c;可以设置选项卡的字体方向为从左到右或从右到左。 myTabWidget QTabWidget() myTabWidget.setTabP…

【论文精读】DALLE3:Improving Image Generation with Better Captions 通过更好的文本标注改进图像生成

文章目录 一、文章概览二、数据重标注&#xff08;一&#xff09;现在训练数据的文本标注主要存在的问题&#xff08;二&#xff09;创建图像标注器&#xff08;三&#xff09;微调图像标注器 三、评估重新标注的数据集&#xff08;一&#xff09;混合合成标注和真实标注&#…

【vue】vue中的路由vue-router,vue-cli脚手架详细使用教程

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

PCD8000D开关调光/调色线性恒流LED控制芯片 无需变压器及电阻电容 只需极少元器件

概述 PCD8000D 是一款开关调节亮度/色温的LED恒流驱动IC。适用于AC 180V-240V 或AC 90V- 130V 输入电压&#xff0c;恒流精度小于 5% 。PCD8000D在3 段调节亮度应用中&#xff0c;可根据开启/关闭电源&#xff0c;依次改变输出电流的大小&#xff0c;从而改变LED 灯的亮度, …

RK3568驱动指南|第二篇 字符设备基础-第13章 杂项设备驱动实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

SpringMVC | SpringMVC中的 “JSON数据交互“ 和 “RESTful设计风格“

目录: 一、JSON 数据交互1.1 JSON概述1.2 JSON的“数据结构”对象结构数组结构 1.3 JSON的“数据转换”用 \<mvc:annotation-driven/>的方式 来“自动配置” MappingJackson2HttpMessageConverter转换器 (来完成JSON数据转换)用\<bean>标签方式的来“自行配置” JS…

Midjourney发布新特性风格参考

1. 引言 最近&#xff0c;Midjourney 推出了Style Reference V2.0 即功能更加强大的风格参考工具&#xff0c;该工具可以让大家参考其他图像的风格&#xff0c;生成与参考图像风格保持一致&#xff0c;与文本提示词语义内容保持一致的图像。它与图像提示类似&#xff0c;但是只…

关于Count,FPKM,TPM,RPKM等表达量的计算

原文链接&#xff1a;关于Count&#xff0c;FPKM&#xff0c;TPM&#xff0c;RPKM等表达量的计算及转换 | 干货 写在前面 今天使用count值转化TPM&#xff0c;或是使用FPKM转换成TPM。这样的教程&#xff0c;我们在前面已经出国一起相对比较详细的教程了&#xff0c;一文了解…

(一)基于IDEA的JAVA基础1

Java是一门面向对象的编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表&#xff0c;极好地实现了面向对象理论&#xff0…

上位机图像处理和嵌入式模块部署(qmacvisual模板匹配)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 模板匹配是图像中经常使用到的功能。我看过很多现场配置部署的同学&#xff0c;虽然他们使用的是类似于vision master、visio pro这样的专业机器视…

STM32实验DMA数据搬运小助手

本次实验做的是将一个数组的内容利用DMA数据搬运小助手搬运到另外一个数组中去。 最后的实验结果&#xff1a; 可以看到第四行的数据就都不是0了&#xff0c;成功搬运了过来。 DMA实现搬运的步骤其实不是很复杂&#xff0c;复杂的是结构体参数&#xff1a; 整个步骤为&#xf…

Day17|二叉树part04:110.平衡二叉树、257.二叉树的所有路径、404.左叶子之和、543: 二叉树的直径、124: 二叉树的最大路径和

之前的blog链接&#xff1a;https://blog.csdn.net/weixin_43303286/article/details/131982632?spm1001.2014.3001.5501 110.平衡二叉树 本题中&#xff0c;一棵高度平衡二叉树定义为&#xff1a;一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。思路&#xff…

【氮化镓】利用Ga2O3缓冲层改善SiC衬底AlN/GaN/AlGaN HEMT器件性能

Micro and Nanostructures 189 (2024) 207815文献于阅读总结。 本文是关于使用SiC衬底AlN/GaN/AlGaN高电子迁移率晶体管&#xff08;HEMT&#xff09;的研究&#xff0c;特别是探讨了不同缓冲层对器件性能的影响&#xff0c;以应用于高速射频&#xff08;RF&#xff09;应用。…

网络抓包原理及常用抓包工具

本文以App作为例子&#xff0c;实际应用不限于App范围。 定位网络接口问题分析其他App数据接口学习网络协议&#xff0c;使用抓包工具分析网络数据更直观 大部分场合都可以通过程序调试来定位问题&#xff0c;但有些场景使用抓包来定位接口问题更准确、更方便&#xff0c;如以…

手机网页关键词视频爬虫采集软件可导出视频分享链接|视频无水印批量下载工具

全新音视频批量下载工具&#xff0c;为您解放视频管理烦恼&#xff01; 现如今&#xff0c;音上涌现出大量精彩的视频内容&#xff0c;但是要想高效地获取、管理和分享这些视频却是一件颇具挑战的事情。针对这一难题&#xff0c;我们自主研发了全新的音视频批量下载工具&#x…

云计算系统等保测评对象和指标选取

1、云计算服务模式与控制范围关系 参考GBT22239-2019《基本要求》附录D 云计算应用场景说明。简要理解下图&#xff0c;主要是云计算系统安全保护责任分担原则和云服务模式适用性原则&#xff0c;指导后续的测评对象和指标选取。 2、测评对象选择 测评对象 IaaS模式 PaaS模式…

微信投票小程序源码系统:礼物道具投票盈利能力超强 带完整的安装代码包以及安装部署教程

近年来&#xff0c;微信小程序以其便捷性、轻量化等特点&#xff0c;迅速占据了移动应用市场的一席之地。投票小程序作为其中的一种应用类型&#xff0c;因其独特的互动性和社交性&#xff0c;成为了商家进行品牌宣传、活动推广的有力工具。然而&#xff0c;市场上的投票小程序…