文章目录
- [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关键字定义的,那就代表了用到了外部链接库
在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的前面的字节进行一定的运算得到的,又因为,实际上v4
与str[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/