Reverse学习日记(一)
逆向工程简介
逆向工程(Software Reverse Engineering),又称软件反向工程,是指从可运行的程序系统出发,运用解密、反汇编、系统分析、程序算法理解等多种计算机技术,对软件的结构、流程、代码等进行逆向拆解和分析,从而推导出软件产品的源代码、设计原理、结构、算法、处理过程、运行方法及相关文档等。
通常,人们把对软件进行反向分析的整个过程统称为软件逆向工程,把在这个过程中所采用的技术都统称为软件逆向工程技术。在CTF中的逆向,需要涉及Windows、Linux、Android平台的多种编程技术。
静态分析技术
不执行程序条件下对源代码进行分析,找出代码缺陷。一般利用静态程序分析工具(例如IDA)将二进制可执行文件翻译成汇编代码或者c语言伪代码,通过对代码分析来破解文件。
动态调试技术
动态调试是指利用调试器软件的运行来破解,观察程序在运行过程中的状态。注意关注代码流和数据流。
壳
在计算机安全领域中,壳(Packers或Protection)指在一个程序外面包裹一段代码,保护里面的代码不被正确反编译或者非法修改,可以用于隐藏程序的功能和信息,常被用于保护软件的版权或防止软件被病毒等恶意软件攻击。
在加载内存时,壳要先于程序运行,保护和加载程序。
加壳就是利用特殊的算法对可执行文件进行压缩,但是跟压缩包不同,解压壳过程在内存中完成,是完全隐蔽的,所以加壳过的程序可以直接运行,但是无法查看源代码,需要进行脱壳。
壳的工作原理是将程序打包成一种自定义的格式,再通过一个小的解压程序来还原原始的程序。壳的类型有很多种,例如UPX,ASPack,Themida,PECompact,以及国内的神仙加壳等,不同的壳技术使用的压缩算法或加密算法也不一样,因此有时候需要使用专门的工具来分析和解壳。
逆向流程
1.利用工具初步了解程序信息,例如studyPE等。
2.静态分析:使用IDA等静态分析工具,对静态信息进行google搜索。
3.绕过保护机制:研究程序的保护方法,例如代码混淆,保护壳和反调试(检测运行时间)等,想办法破除和绕过机制。
4.定位关键代码。
5.动态分析:使用动态调试工具运行程序,验证猜想并理清程序功能。
6.根据程序功能写出脚本,完成逆向分析。
C语言编译原理
计算机能理解的只有0和1,任何程序要执行都要转化为二进制才能被计算机理解。
这里以一个命名为main.c
的文件举例:
1 |
|
gcc预处理
gcc -E main.c -o main.i
gcc是编译器,-E
表示预处理,-o
表示指定输出文件。
main.i
:
1 | # 1 "main.c" |
预处理阶段主要进行:
1.头文件包含:处理#include
预编译指令,将被包含内容插入到预编译位置。
2.宏替换:删除所有#define
,展开所有的宏定义,例如这里有#define Pi 3.1415926
,预处理就将程序中所有的Pi
换成具体数值。
3.处理所有条件编译指令:例如if ifdef elif else endif
,决定哪些代码被编译,哪些不被编译。
4.删除所有注释。
5.保留#pargma
编译器指令。
编译
将预处理完的文件进行词法、语法、语义分析优化后生成汇编代码文件。
gcc -S -masm=intel main.i -o main.s
得到的main.s
:
1 | .file "main.c" |
汇编
汇编器将汇编代码转变为机器码(机器可以执行的指令):
gcc -c main.s -o main.o
得到的文件已经是二进制文件了,可以使用objdump -sd main.o
显示:
1 | main.o: file format pe-x86-64 |
链接
目标文件还需要链接一大堆文件才能得到最终的可执行文件,连接过程包括地址和空间分配,符号决议和重定向等:gcc main.o -o main.exe
汇编语言
在汇编语言中用助记符代替机器指令的操作码,用地址符号或者标号代替指令或者操作数的地址。不同设备中汇编语言对应着不同的机器语言指令集,和CPU架构有关, 不同架构的CPU指令不同。
除了汇编语言还存在机器语言和高级语言:
计算机的硬件作为一种电路元件,它的输出和输入只能是有电或者没电,也就是所说的高电平和低电平,所以计算机传递的数据是由“0”和“1”组成的二进制数。因此,二进制的语言是计算机语言的本质。计算机发明之初,人们为了去控制计算机完成自己的任务或者项目,只能去编写“0”、”1”这样的二进制数字串去控制电脑,其实就是控制计算机硬件的高低电平或通路开路,这种语言就是机器语言。
高级语言可以不依赖于计算机硬件,在不同机器上运行,C、java、python等都是高级语言。
声明段
.text
:代码段,用于存放可执行程序,也就是对应架构下程序的指令序列。
.data
:数据段,用于存放编译时就能确定的全局数据。
.rodata
:只读数据段,存放的是只读数据,比如字符串常量、全局const变量。
.bss
:用于存放编译阶段无法确定的全局数据,包括未初始化的全局变量和静态变量。特点是可读写的,在程序执行之前BSS段会自动清0。
.comment
:存放的是编译器版本等信息。
.eh_frame
:调试信息段。
常用指令
汇编语言指令是构成汇编程序的基本操作指令,先总体介绍一下常用指令:
- 数据移动指令:
mov
:用于数据在寄存器和内存之间的传递。push
:将数据压入栈中。pop
:从栈中弹出数据。lea
:取地址运算。
- 逻辑运算指令:
add
,sub
,mul
,div
:两个操作数加减乘除运算。and
,xor
,or
:对两个操作数执行按位与、异或、或运算。inc
,dec
:自增自减运算。shl
,shr
:按位左移或者右移,空位补0。
- 流程控制指令:
jmp
:无条件跳转。je
、jne
、jz
、jnz
:条件跳转指令,根据条件码执行跳转。call
:调用一个过程或函数。ret
:子函数返回,返回上一层函数。
- 比较指令:
cmp
:比较两个操作数。
- 循环指令:
loop
:循环的执行指令块。
- 输入输出指令:
in
:从设备或端口输入数据。out
:向设备或端口输出数据。
间接寻址
在汇编语言中,间接寻址(Indirect Addressing)用于引用存储在内存中的数据。相比于直接寻址,间接寻址允许使用寄存器或内存中的地址作为操作数,从而更加灵活地进行内存操作。
在间接寻址模式下,使用方括号 []
将操作数包裹起来,以指示寻址的方式。方括号内可以是寄存器、内存地址或者寄存器与偏移值的组合。
下面是一些常见的间接寻址示例:
- 使用寄存器作为操作数,例如:
[rax]
、[rcx]
、[rdx]
等。该示例表示根据rax
、rcx
、rdx
寄存器中的值作为内存地址来访问该地址上存储的数据。 - 使用内存地址作为操作数,例如:
[0x12345678]
、[myVariable]
等。该示例表示访问指定内存地址中存储的数据。 - 使用寄存器与偏移值的组合作为操作数,例如:
[rbx + 8]
、[rsi - 16]
等。该示例表示根据寄存器的值与指定的偏移值计算出的内存地址来访问存储在该地址上的数据。
mov
mov指令是用于数据传送的指令,它可以将一个数据从一个位置复制到另一个位置。具体的用法如下:
1 | mov destination, source |
destination
是数据将要存储的位置。source
是数据的来源。
mov
指令可以用于以下几种情况:
1.寄存器到寄存器的传送:
1 | mov destination_register, source_register |
这条指令将把source_register
寄存器中的值复制到destination_register
寄存器中,例如:
1 | mov eax, ebx |
这个例子将ebx
寄存器的值传送到eax
寄存器中。即为eax=ebx
2.立即数到寄存器的传送:
1 | mov destination_register, immediate_value |
这条指令将把一个立即数(immediate_value
)复制到destination_register
寄存器中,例如:
1 | mov ecx, 10 |
这个例子将立即数10
传送到ecx
寄存器中。
立即数(immediate value)是汇编语言中的一个概念,表示一个固定的常数值。它可以直接在指令中使用,而不需要从寄存器或内存中加载。
立即数可以用于执行各种操作,如加法、减法、移位等。立即数可以是有符号数(带符号的整数)或无符号数(正整数)。
汇编代码中立即数通常以不同的表示形式出现:
- 十进制表示:使用十进制数表示立即数,例如
10
、25
。- 十六进制表示:使用前缀
0x
(或0X
)加上十六进制数表示立即数,例如0xA
、0x1F
。- 二进制表示:使用前缀
0b
(或0B
)加上二进制数表示立即数,例如0b1010
、0b11111
。- 字符常量:使用单引号或双引号括起来的字符常量,例如
'A'
、"Hello"
。在汇编语言中,字符常量的值通常是根据其 ASCII 或 Unicode 编码确定的。以下是示例代码,展示了立即数在汇编语言中的使用:
1
2
3
4 mov eax, 10 ; 将立即数 10 存储到寄存器 eax 中
add eax, 0x0F ; 将十六进制立即数 0x0F 加到寄存器 eax 中
sub ebx, 0b1101 ; 将二进制立即数 0b1101 从寄存器 ebx 中减去
mov dl, 'A' ; 将字符常量 'A' 存储到寄存器 dl 中在上述示例中,立即数直接作为指令的操作数使用,不需要像寄存器或内存一样进行加载。它们用于表示操作和数据的固定值。
3.内存到寄存器的传送:
1 | mov destination_register, [memory_address] |
这条指令将把存储在memory_address
内存地址中的值复制到destination_register
寄存器中,例如:
1 | mov edx, [ebp-4] |
这个例子从存储在ebp-4
内存地址中的值传送到edx
寄存器中。假设ebp
寄存器保存了当前栈帧的基指针。
4.寄存器到内存的传送:
1 | mov [memory_address], source_register |
这条指令将把source_register
寄存器中的值复制到memory_address
内存地址中,例如:
1 | mov [ebp+8], eax |
这个例子将eax
寄存器的值传送到位于ebp+8
偏移量的内存位置。
5.传送字符串:
1 | mov destination_string, source_string |
在汇编语言中,字符串是由一系列字符组成的数据。使用mov
指令可以将源字符串复制到目标字符串的位置,举个例子:
1 | mov edi, offset myString |
在这个例子中,mov edi, offset myString
将字符串 myString
的地址赋值给目标寄存器 edi
。offset
是一个操作符,用于获取标签或变量在内存中的偏移量。
mov esi, offset buffer
将缓冲区 buffer
的地址赋值给源寄存器 esi
。
mov ecx, length
将 length
的值赋值给计数寄存器 ecx
,length
表示要复制的字节数。
rep movsb
是一个指令,它表示重复执行 movsb
指令(每次传送一个字节),直到 ecx
寄存器的值变为零。在这个例子中,它实际上将字符串 myString
中的内容复制到缓冲区 buffer
中。
这段代码的目的是字符串拷贝,它使用了 edi
、esi
、ecx
以及 rep movsb
指令来完成。
add
add
是汇编指令中用于执行加法操作的操作码。它可以用于将两个操作数相加,并将结果存储在目标操作数中。
add
指令的格式为:
1 | add destination, source |
其中,destination
表示目标操作数,可以是寄存器、内存地址或立即数;source
表示源操作数,可以是寄存器、内存地址或立即数。
add
指令对于不同的操作数类型,有不同的含义:
- 如果
destination
和source
都是寄存器或者内存地址,则将source
的值加到destination
中,并将结果存储在destination
中。 - 如果
source
是立即数,则将立即数的值加到destination
中,并将结果存储在destination
中。
举例:
1.将两个寄存器相加并将结果存储在目标寄存器中:
- 将两个寄存器相加并将结果存储在目标寄存器中:
1 | mov eax, 10 ; 将值 10 存储在寄存器 eax 中 |
2.将内存地址中的值与立即数相加并将结果存储回内存地址中:
1 | section .data |
3.将立即数与寄存器中的值相加并将结果存储在目标寄存器中:
1 | mov eax, 10 ; 将值 10 存储在寄存器 eax 中 |
sub
sub 汇编指令用于执行两个操作数之间的减法运算。它的用法类似于 add 指令,只是它执行减法而不是加法。
下面是 sub 指令的一般语法:
1 | sub 目的操作数, 源操作数 |
在汇编语言中,目的操作数和源操作数可以是寄存器、内存地址或立即数。
以下是一些示例,展示了如何使用 sub 指令进行减法运算:
1 | mov eax, 10 ; 将 10 存储到寄存器 eax 中 |
在上面的示例中,sub 指令执行了减法运算,并将结果存储回目标操作数中。因此,减法运算的结果可以在目标操作数中进行后续使用。
ret
汇编语言中 ret
(返回)指令用于从一个子程序或函数中返回到调用它的位置。当一个子程序完成其任务时,可以使用 ret
指令将控制权返回给调用者。
ret
指令没有操作数,它只是简单地从栈中弹出保存的返回地址,并将控制权转移回该地址。在调用子程序或函数时,返回地址会被压入栈中,以便在子程序执行完毕后返回到正确的位置。
以下是一个例子来展示 ret
指令的使用:
1 | section .text |
在汇编语言中,
section
是用来定义不同类型的程序段的指令。可以使用section
指令来创建段,并使用相应的指令和标记符在段中定义数据、指令和其他内容。程序段是将源代码划分为逻辑块的一种方式,每个段都具有特定的属性和用途。常见的几种类型的程序段包括:
.data
:用于定义静态数据(如全局变量、常量等)的段。.text
:用于存放程序的可执行代码的段。.bss
:用于存放未初始化的全局变量和静态变量的段。在程序运行时,这些变量会被自动初始化为零或者默认值。.rodata
:用于存放只读数据(如常量字符串、只读变量等)的段。
在上面的例子中,程序从 _start
标签开始执行,然后调用了 myFunction
函数。当执行到 ret
指令时,它会从栈中弹出保存的返回地址,并将控制权转移到该地址,即回到 _start
标签后的指令处。
需要注意的是,栈中保存的返回地址是由调用者负责压入栈中的。在 x86 架构中,通常使用 call
指令调用函数,该指令会自动将返回地址压入栈中,然后跳转到函数的入口点执行。因此,在函数的结尾使用 ret
指令可以正确地返回到调用者的位置。
call
在汇编语言中,call
指令用于调用一个子程序(函数)或跳转到一个指定的子过程或标签位置。它的作用类似于C语言中的函数调用。
当使用 call
指令时,通常会将被调用的子程序的入口地址推入堆栈,并将程序的控制转移到被调用的子程序中。被调用的子程序执行完后,使用 ret
(返回)指令将控制返回给调用者,同时从堆栈中弹出被调用子程序的返回地址。
下面是一个使用 call
指令的示例:
1 | section .data |
在上面的示例中,使用了两个 call
指令来调用两个子程序:printMessage
和 exitProgram
。printMessage
子程序负责打印消息 “Hello, World!”,而 exitProgram
子程序负责终止程序。
在这个示例中,我们使用 mov
指令将需要的参数设置到寄存器中(例如:系统调用号、文件描述符、消息地址、消息长度等),然后使用 syscall
指令调用相应的系统调用。最后,使用 ret
指令将控制返回给调用者。
lea
lea
(load effective address)用于计算和加载有效地址(Effective Address)。
lea
指令的语法如下:
1 | lea destination, source |
lea
指令将 source
的地址计算出来,并将结果加载到 destination
中。需要注意的是,lea
指令并不是用于加载数据到寄存器,而是用于计算地址并将计算结果存储在目标操作数中。
在汇编语言中,lea
指令通常与间接寻址模式一起使用,来计算一个变量或数组元素的地址。它可以用于进行一些高级计算,例如计算数组元素的偏移量、计算结构体成员的偏移量等。
下面是一个使用 lea
指令计算数组元素地址的简单示例:
1 | section .data |
在上面的示例中,我们有一个名为 myArray
的数组,其中包含一些dq
(64位整数)值。我们使用 lea
指令计算了 myArray[2]
的地址,并将结果保存到 rax
寄存器中。然后,我们可以使用 rax
来访问该数组元素的值。在示例中,我们将 myArray[2]
的值保存到 rbx
寄存器中。
需要注意的是,myArray
在内存中是以字节为单位进行存储的,每个dq
占据 8 个字节。因此,在计算数组元素地址时,我们使用 myArray + 2 * 8
来计算 myArray[2]
的地址。
push和pop
push 和 pop 分别用于将数据压入堆栈和从堆栈中弹出数据。
- Push(入栈)指令将数据压入堆栈。它的典型用法是
push operand
,其中operand
可以是寄存器、内存地址或立即数。这条指令会将操作数的值放入堆栈的顶部,并将堆栈指针减小以指向新的顶部。如下是一些示例:push eax
:将eax
寄存器的值压入堆栈。push [ebx]
:将存储在ebx
寄存器指向的内存地址中的值压入堆栈。push 42
:将立即数值 42 压入堆栈。
- Pop(出栈)指令从堆栈中弹出数据。它的典型用法是
pop operand
,这条指令会将堆栈顶部的值弹出,并将其存储到指定的操作数中,然后将堆栈指针增加以指向新的顶部。以下是一些示例:pop ebx
:将堆栈顶部的值弹出,并将其存储到ebx
寄存器中。pop [eax]
:将堆栈顶部的值弹出,并将其存储到eax
寄存器指向的内存地址中。
push 和 pop 指令还可以与其他指令结合使用,例如在函数调用时通过 push 把参数传递给函数,然后通过 pop 来恢复堆栈。
寄存器
一个典型的CPU由运算器、控制器和寄存器等器件组成。运算器进行信息处理,寄存器进行信息存储,控制器控制各种器件进行工作,这些器件由内部总线链接,进行数据传送。
寄存器是计算机暂存指令、数据和地址的地方,访问速度比内存快得多,价格更高。
常用寄存器分为四类:
1.八个32位通用寄存器:(数据存储器)EAX、EBX、ECX、EDX;(指针变址寄存器)EBP、ESP、ESI、EDI。
2.六个16位段寄存器:CS、SS、DS、ES、FS、GS
3.一个32位标志寄存器:EFLAGS
4.一个32位指令指针寄存器:EIP
64位:对应RSP,RBP,RIP三个寄存器。64bit=8字节。
32位:对应RSP,EBP,EIP三个寄存器。32bit=4字节。
数据寄存器
数据寄存器(EAX、EBX、ECX、EDX)主要用于各种运算和数据的传送,每个数据寄存器都可以作为一个32位、16位或8位来使用。
以寄存器EAX为例:EAX寄存器可以存储32位的数据,EAX的低16位可以表示为AX,可以存储16位的数据。AX寄存器又可分为AH和AL两个8位的寄存器,AH对应AX寄存器的高8位,AL对应AX寄存器的低8位:
其他几个数据寄存器同样有这个特性,注意这几个寄存器的默认作用,实际编写汇编代码时候不是必须按照这个用途使用:
EAX:累加寄存器,用作很多算术运算。
EBX:基地址寄存器,在内存寻址时存放基地址。
ECX:计数器,保存循环索引。
EDX:被用于来放整数除法产生的余数。
指针变址寄存器
指针变址寄存器(EBP、ESP、ESI、EDI)可以按照32位或16位进行使用,但是无法分割为8位来使用。
比如ESI寄存器:可以存储32位的指针,其中低16位可以表示为SI,存储16位的指针,但是无法像AX那样拆分成高8位和低8位。
同样地,其他几个数据寄存器同样有这个特性,下面是这几个寄存器的默认用途:
EBP:堆栈基址指针寄存器,可以用来访问堆栈中的数据。
ESP:专门用作堆栈指针,被形象地称为栈顶指针,配合EBP来访问栈顶数据。
ESI:(source)源指针寄存器,用于内存数据的传送,DS:ESI指向源串。
EDI: (destination)目的指针寄存器,用于内存数据的传送,DS:EDI指向目的串。
段寄存器
段寄存器被用于存放段的基地址,段是一块预分配的内存区域。
有些段存放有程序的指令,有些则存放有程序的变量,另外还有其他的段,如堆栈段存放着函数变量和函数参数等。在16位CPU中,段寄存器只有4个,分别是CS(代码段code segment) 、DS(数据段)、SS(堆栈段)和ES(附加段)。在32位CPU中,段寄存器从4个扩展为6个,分别是CS、DS、SS、ES、FS和GS。FS和GS段寄存器也属于附加的段寄存器。
指令指针寄存器
EIP(16位CPU叫做IP)保存着下一条要执行的指令的地址(CS:EIP),顺序执行汇编代码,下一条指令的地址为当前指令的地址加当前指令的长度(EIP=EIP+Length),遇到JMP、JE、LOOP等跳转指令就会指定EIP值为跳转目的地,导致CPU执行指令产生跳跃性执行,从而构成分支与循环结构。
标志寄存器
EFLAGS(16位为FLAGS,无论是32位还是16位的寄存器都只用前16位。)保存一些运算的结果。
简单题目
SWPUCTF 2021 新生赛
re1
给了exe文件,首先用PEiD查壳,没有壳。
然后用die查看详细信息,发现为64位文件,用64位IDA打开,先用F5打开伪代码:
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
可以发现这个程序类似一个替换加密,写个程序解密一下:
1 | f= '{34sy_r3v3rs3}' |
得到字段{easy_reverse}
。
简简单单的逻辑
1 | flag = 'xxxxxxxxxxxxxxxxxx' |
一道python题,还好我学过密码:
1 | list = [47, 138, 127, 57, 117, 188, 51, 143, 17, 84, 42, 135, 76, 105, 28, 169, 25] |
简简单单的解密
1 | import base64,urllib.parse |
一道python加密题目,urllib.parse这个库用来进行url加密,加密用了quote函数,可以查到解密用unquote函数。主体的s盒加密都是可逆的加加减减异或运算:
1 | s_box = list(range(256)) |
实际上,真正加密只用了最后一个异或chr(ord(s) ^ k)
,直接利用加密代码得到移位等操作后的s盒直接对密文异或即可:
1 | import base64, urllib.parse |
非常简单的逻辑题
1 | flag = 'xxxxxxxxxxxxxxxxxxxxx' |
又是一道python题,想到两种方法可以做,一种是正着遍历ASCII值得到相同输出得到flag:
1 | s = 'wesyvbniazxchjko1973652048@$+-&*<>' |
另一种是根据已知的result反推出原字母,因为构成flag的字符的ASCII值不会出128,所以:
$$
s_1=[\frac j {17}]
$$
$$
s_2=j\bmod 17
$$
$$
j=17s_1+s_2
$$
1 | s = 'wesyvbniazxchjko1973652048@$+-&*<>' |
fakerandom
1 | import random |
加密用了随机数,种子都是人工设好的,加密就一个对称的异或,直接按照题目的代码把result和得到的随机数挨个异或即可:
1 | import random |
fakebase
1 | flag = 'xxxxxxxxxxxxxxxxxxx' |
在题目代码中,前半部分其实是熟悉的long_to_bytes
过程:
1 | flag = 'xxxxxxxxxxxxxxxxxxx' |
所以重点在最后这部分:
1 | s = '' |
数值b1每一轮都整除31,并在s字符串中加入取模31后的结果,很明显这里需要逆推回去,知道:
$$
s=b_1\bmod 31
$$
$$
b_1=s+31k
$$
k是未知的,但是可以爆破,得知了第一轮的k之后,就可以反复逆推回最初的b1了:
1 | from Crypto.Util.number import * |
在一堆得到的解码值中能找到flag。
easyapp
一道安卓逆向题,用java编写的,可以使用JADX进行安卓反编译。
打开之后寻找有用的代码,在com文件夹里有个叫MainActivity的,打开,发现:
1 | public /* synthetic */ void lambda$onCreate$0$MainActivity(final EditText editText, View v) { |
这个方法的函数名为”lambda$onCreate0MainActivity”,它的参数是一个EditText和一个View。该方法首先使用encoder对EditText中的文本进行编码,然后将其与”棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌”进行比较。如果两者相等,则Toast消息将显示为“YES”,否则显示为“NO”。
在旁边的另一个程序里发现了encoder:
1 | /* loaded from: classes.dex */ |
该方法接受一个字符串作为输入,并返回一个经过简单异或加密后的字符串。在该方法内部,input字符串的每个字符都通过XOR运算与”key”变量进行加密。返回加密后的字符串。
在com里还找到个MainActlvity,它试图访问Encoder类中的”key”变量,并将变量的值设置为987654321:
1 | public class MainActlvity { |
所以,这整个程序逻辑就是个简单的异或,直接异或回去就行了:
1 | code = '棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌' |
SWPUCTF 2022 新生赛
easyre
无壳64位,IDAshift+f12就看到了。看看伪代码:
1 | _main(); |
base64
无壳64位,IDA打开,一看找到base64加密的密文,还有一个base64字母表,直接解密:
1 | import base64 |
[BJDCTF 2020]JustRE
exe文件,无壳32位,打开之后找字符串,发现一个长得很像flag的东西:`.data:00407030 aBjdDD2069a4579 db ‘ BJD