逆向工程简介

逆向工程(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
2
3
4
5
6
#include <stdio.h>

int main(){
printf("HelloWorld\n");
return 0;
}

gcc预处理

gcc -E main.c -o main.i

gcc是编译器,-E表示预处理,-o表示指定输出文件。

main.i

1
2
3
4
5
6
7
8
9
10
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
......
# 3 "main.c"
int main(){
printf("HelloWorld\n");
return 0;
}

预处理阶段主要进行:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	.file	"main.c"
.intel_syntax noprefix
.text
.def __main; .scl 2; .type 32; .endef
.section .rdata,"dr"
.LC0:
.ascii "HelloWorld\n\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 32
.seh_stackalloc 32
.seh_endprologue
call __main
lea rcx, .LC0[rip]
call printf
mov eax, 0
add rsp, 32
pop rbp
ret
.seh_endproc
.ident "GCC: (x86_64-win32-seh-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; .scl 2; .type 32; .endef

汇编

汇编器将汇编代码转变为机器码(机器可以执行的指令):

gcc -c main.s -o main.o

得到的文件已经是二进制文件了,可以使用objdump -sd main.o显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
main.o:     file format pe-x86-64

Contents of section .text:
0000 554889e5 4883ec20 e8000000 00488d0d UH..H.. .....H..
0010 00000000 e8000000 00b80000 00004883 ..............H.
0020 c4205dc3 90909090 90909090 90909090 . ].............
Contents of section .rdata:
0000 48656c6c 6f576f72 6c642f6e 00000000 HelloWorld/n....
Contents of section .xdata:
0000 01080305 08320403 01500000 .....2...P..
Contents of section .pdata:
0000 00000000 24000000 00000000 ....$.......
Contents of section .rdata$zzz:
0000 4743433a 20287838 365f3634 2d77696e GCC: (x86_64-win
0010 33322d73 65682d72 6576302c 20427569 32-seh-rev0, Bui
0020 6c742062 79204d69 6e47572d 57363420 lt by MinGW-W64
0030 70726f6a 65637429 20382e31 2e300000 project) 8.1.0..

Disassembly of section .text:

0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 20 sub $0x20,%rsp
8: e8 00 00 00 00 callq d <main+0xd>
d: 48 8d 0d 00 00 00 00 lea 0x0(%rip),%rcx # 14 <main+0x14>
14: e8 00 00 00 00 callq 19 <main+0x19>
19: b8 00 00 00 00 mov $0x0,%eax
1e: 48 83 c4 20 add $0x20,%rsp
22: 5d pop %rbp
23: c3 retq
24: 90 nop
25: 90 nop
26: 90 nop
27: 90 nop
28: 90 nop
29: 90 nop
2a: 90 nop
2b: 90 nop
2c: 90 nop
2d: 90 nop
2e: 90 nop
2f: 90 nop

链接

目标文件还需要链接一大堆文件才能得到最终的可执行文件,连接过程包括地址和空间分配,符号决议和重定向等: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:调试信息段。

常用指令

汇编语言指令是构成汇编程序的基本操作指令,先总体介绍一下常用指令:

  1. 数据移动指令:
    • mov:用于数据在寄存器和内存之间的传递。
    • push:将数据压入栈中。
    • pop:从栈中弹出数据。
    • lea:取地址运算。
  2. 逻辑运算指令:
    • add,sub,mul,div:两个操作数加减乘除运算。
    • and,xor,or:对两个操作数执行按位与、异或、或运算。
    • inc,dec:自增自减运算。
    • shl,shr:按位左移或者右移,空位补0。
  3. 流程控制指令:
    • jmp:无条件跳转。
    • jejnejzjnz:条件跳转指令,根据条件码执行跳转。
    • call:调用一个过程或函数。
    • ret:子函数返回,返回上一层函数。
  4. 比较指令:
    • cmp:比较两个操作数。
  5. 循环指令:
    • loop:循环的执行指令块。
  6. 输入输出指令:
    • in:从设备或端口输入数据。
    • out:向设备或端口输出数据。

间接寻址

在汇编语言中,间接寻址(Indirect Addressing)用于引用存储在内存中的数据。相比于直接寻址,间接寻址允许使用寄存器或内存中的地址作为操作数,从而更加灵活地进行内存操作。

在间接寻址模式下,使用方括号 [] 将操作数包裹起来,以指示寻址的方式。方括号内可以是寄存器、内存地址或者寄存器与偏移值的组合。

下面是一些常见的间接寻址示例:

  1. 使用寄存器作为操作数,例如:[rax][rcx][rdx] 等。该示例表示根据 raxrcxrdx 寄存器中的值作为内存地址来访问该地址上存储的数据。
  2. 使用内存地址作为操作数,例如:[0x12345678][myVariable] 等。该示例表示访问指定内存地址中存储的数据。
  3. 使用寄存器与偏移值的组合作为操作数,例如:[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)是汇编语言中的一个概念,表示一个固定的常数值。它可以直接在指令中使用,而不需要从寄存器或内存中加载

立即数可以用于执行各种操作,如加法、减法、移位等。立即数可以是有符号数(带符号的整数)或无符号数(正整数)。

汇编代码中立即数通常以不同的表示形式出现:

  • 十进制表示:使用十进制数表示立即数,例如 1025
  • 十六进制表示:使用前缀 0x(或 0X)加上十六进制数表示立即数,例如 0xA0x1F
  • 二进制表示:使用前缀 0b(或 0B)加上二进制数表示立即数,例如 0b10100b11111
  • 字符常量:使用单引号或双引号括起来的字符常量,例如 '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
2
3
4
mov edi, offset myString
mov esi, offset buffer
mov ecx, length
rep movsb

在这个例子中,mov edi, offset myString 将字符串 myString 的地址赋值给目标寄存器 edioffset 是一个操作符,用于获取标签或变量在内存中的偏移量。

mov esi, offset buffer 将缓冲区 buffer 的地址赋值给源寄存器 esi

mov ecx, lengthlength 的值赋值给计数寄存器 ecxlength 表示要复制的字节数。

rep movsb 是一个指令,它表示重复执行 movsb 指令(每次传送一个字节),直到 ecx 寄存器的值变为零。在这个例子中,它实际上将字符串 myString 中的内容复制到缓冲区 buffer 中。

这段代码的目的是字符串拷贝,它使用了 ediesiecx 以及 rep movsb 指令来完成。

add

add 是汇编指令中用于执行加法操作的操作码。它可以用于将两个操作数相加,并将结果存储在目标操作数中

add 指令的格式为:

1
add destination, source

其中,destination 表示目标操作数,可以是寄存器、内存地址或立即数;source 表示源操作数,可以是寄存器、内存地址或立即数。

add 指令对于不同的操作数类型,有不同的含义

  • 如果 destinationsource 都是寄存器或者内存地址,则将 source 的值加到 destination 中,并将结果存储在 destination 中。
  • 如果 source 是立即数,则将立即数的值加到 destination 中,并将结果存储在 destination 中。

举例:

1.将两个寄存器相加并将结果存储在目标寄存器中:

  1. 将两个寄存器相加并将结果存储在目标寄存器中:
1
2
3
4
5
6
mov eax, 10  ; 将值 10 存储在寄存器 eax 中
mov ebx, 5 ; 将值 5 存储在寄存器 ebx 中

add eax, ebx ; 将寄存器 ebx 的值加到寄存器 eax 中

; 此时,寄存器 eax 的值为 15

2.将内存地址中的值与立即数相加并将结果存储回内存地址中:

1
2
3
4
5
6
7
8
9
10
11
section .data
myNum dw 10 ; 在数据段中定义一个字(16位)的变量 myNum,并初始化为值 10

section .text
mov esi, offset myNum ; 将 myNum 的内存地址存储在寄存器 esi 中

mov word [esi], 10 ; 将立即数 10 存储到 myNum 地址处

add word [esi], 5 ; 将 myNum 地址处的值加上立即数 5,并将结果存储回该内存地址

; 此时,myNum 的值变为 15

3.将立即数与寄存器中的值相加并将结果存储在目标寄存器中:

1
2
3
4
5
mov eax, 10   ; 将值 10 存储在寄存器 eax 中

add eax, 5 ; 将立即数 5 加到寄存器 eax 中

; 此时,寄存器 eax 的值为 15

sub

sub 汇编指令用于执行两个操作数之间的减法运算。它的用法类似于 add 指令,只是它执行减法而不是加法。

下面是 sub 指令的一般语法:

1
sub 目的操作数, 源操作数

在汇编语言中,目的操作数和源操作数可以是寄存器、内存地址或立即数。

以下是一些示例,展示了如何使用 sub 指令进行减法运算:

1
2
3
4
5
6
7
8
mov eax, 10        ; 将 10 存储到寄存器 eax 中
sub eax, 5 ; 从 eax 寄存器中减去立即数 5,并将结果存储回 eax

mov ebx, 1000h ; 将内存地址 1000h 上的值存储到寄存器 ebx 中
sub ebx, 200h ; 从 ebx 寄存器中减去立即数 200h,并将结果存储回 ebx

mov ecx, dword [esi] ; 将内存地址 esi 中的值存储到寄存器 ecx 中
sub ecx, edx ; 从 ecx 寄存器中减去寄存器 edx 中的值,并将结果存储回 ecx

在上面的示例中,sub 指令执行了减法运算,并将结果存储回目标操作数中。因此,减法运算的结果可以在目标操作数中进行后续使用。

ret

汇编语言中 ret(返回)指令用于从一个子程序或函数中返回到调用它的位置。当一个子程序完成其任务时,可以使用 ret 指令将控制权返回给调用者

ret 指令没有操作数,它只是简单地从栈中弹出保存的返回地址,并将控制权转移回该地址。在调用子程序或函数时,返回地址会被压入栈中,以便在子程序执行完毕后返回到正确的位置。

以下是一个例子来展示 ret 指令的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
section .text
global _start

_start:
call myFunction ; 调用函数 myFunction

; 一些其他指令

mov eax, 1 ; 退出程序
int 0x80

myFunction:
; 函数的代码块

ret ; 返回到调用者的位置

在汇编语言中,section 是用来定义不同类型的程序段的指令。可以使用 section 指令来创建段,并使用相应的指令和标记符在段中定义数据、指令和其他内容。程序段是将源代码划分为逻辑块的一种方式,每个段都具有特定的属性和用途。常见的几种类型的程序段包括:

  1. .data:用于定义静态数据(如全局变量、常量等)的段。
  2. .text:用于存放程序的可执行代码的段
  3. .bss:用于存放未初始化的全局变量和静态变量的段。在程序运行时,这些变量会被自动初始化为零或者默认值
  4. .rodata:用于存放只读数据(如常量字符串、只读变量等)的段。

在上面的例子中,程序从 _start 标签开始执行,然后调用了 myFunction 函数。当执行到 ret 指令时,它会从栈中弹出保存的返回地址,并将控制权转移到该地址,即回到 _start 标签后的指令处。

需要注意的是,栈中保存的返回地址是由调用者负责压入栈中的。在 x86 架构中,通常使用 call 指令调用函数,该指令会自动将返回地址压入栈中,然后跳转到函数的入口点执行。因此,在函数的结尾使用 ret 指令可以正确地返回到调用者的位置。

call

在汇编语言中,call 指令用于调用一个子程序(函数)或跳转到一个指定的子过程或标签位置。它的作用类似于C语言中的函数调用。

当使用 call 指令时,通常会将被调用的子程序的入口地址推入堆栈,并将程序的控制转移到被调用的子程序中。被调用的子程序执行完后,使用 ret(返回)指令将控制返回给调用者,同时从堆栈中弹出被调用子程序的返回地址。

下面是一个使用 call 指令的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
section .data
msg db "Hello, World!", 0

section .text
global _start

_start:
; 打印消息 "Hello, World!"
call printMessage

; 结束程序
call exitProgram

printMessage:
mov rax, 1 ; write 系统调用编号
mov rdi, 1 ; 标准输出文件描述符
mov rsi, msg ; 消息地址
mov rdx, 13 ; 消息长度
syscall ; 调用系统调用

ret ; 返回调用者

exitProgram:
mov eax, 60 ; exit 系统调用编号
xor edi, edi ; 返回码为0
syscall ; 调用系统调用

在上面的示例中,使用了两个 call 指令来调用两个子程序:printMessageexitProgramprintMessage 子程序负责打印消息 “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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
section .data
myArray dq 1, 2, 3, 4, 5

section .text
global _start

_start:
; 计算 myArray[2] 的地址并保存到 rax
lea rax, [myArray + 2 * 8]
; 现在 rax 中存储的是 myArray[2] 的地址

; 之后可以使用 rax 来访问 myArray[2] 的值
mov rbx, [rax]
; 将 myArray[2] 的值保存到 rbx 寄存器中

; 更多操作...

; 退出程序
mov eax, 60
xor edi, edi
syscall

在上面的示例中,我们有一个名为 myArray 的数组,其中包含一些dq(64位整数)值。我们使用 lea 指令计算了 myArray[2] 的地址,并将结果保存到 rax 寄存器中。然后,我们可以使用 rax 来访问该数组元素的值。在示例中,我们将 myArray[2] 的值保存到 rbx 寄存器中。

需要注意的是,myArray 在内存中是以字节为单位进行存储的,每个dq占据 8 个字节。因此,在计算数组元素地址时,我们使用 myArray + 2 * 8 来计算 myArray[2] 的地址。

push和pop

push 和 pop 分别用于将数据压入堆栈从堆栈中弹出数据

  1. Push(入栈)指令将数据压入堆栈。它的典型用法是 push operand,其中 operand 可以是寄存器、内存地址或立即数。这条指令会将操作数的值放入堆栈的顶部,并将堆栈指针减小以指向新的顶部。如下是一些示例:
    • push eax:将 eax 寄存器的值压入堆栈。
    • push [ebx]:将存储在 ebx 寄存器指向的内存地址中的值压入堆栈。
    • push 42:将立即数值 42 压入堆栈。
  2. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int __cdecl main(int argc, const char **argv, const char **envp)
{
char Str2[1008]; // [rsp+20h] [rbp-60h] BYREF
char Str1[1000]; // [rsp+410h] [rbp+390h] BYREF
int i; // [rsp+7FCh] [rbp+77Ch]

_main();
strcpy(Str2, "{34sy_r3v3rs3}");
printf("please put your flag:");
scanf("%s", Str1);
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 101 )
Str1[i] = 51;
}
for ( i = 0; i <= 665; ++i )
{
if ( Str1[i] == 97 )
Str1[i] = 52;
}
if ( strcmp(Str1, Str2) )
printf("you are wrong,see again!");
else
printf("you are right!");
system("pause");
return 0;
}

可以发现这个程序类似一个替换加密,写个程序解密一下:

1
2
3
4
5
6
7
8
9
10
f= '{34sy_r3v3rs3}'
flag=''
for i in f:
if ord(i)==51:
flag+=chr(101)
elif ord(i)==52:
flag+=chr(97)
else:
flag+=i
print(flag)

得到字段{easy_reverse}

简简单单的逻辑

1
2
3
4
5
6
7
8
flag = 'xxxxxxxxxxxxxxxxxx'
list = [47, 138, 127, 57, 117, 188, 51, 143, 17, 84, 42, 135, 76, 105, 28, 169, 25]
result = ''
for i in range(len(list)):
key = (list[i]>>4)+((list[i] & 0xf)<<4)
result += str(hex(ord(flag[i])^key))[2:].zfill(2)
print(result)
# result=bcfba4d0038d48bd4b00f82796d393dfec

一道python题,还好我学过密码:

1
2
3
4
5
6
list = [47, 138, 127, 57, 117, 188, 51, 143, 17, 84, 42, 135, 76, 105, 28, 169, 25]
result='bcfba4d0038d48bd4b00f82796d393dfec'
for i in range(len(list)):
num=int(result[2*i:2*i+2],16)
key = (list[i]>>4)+((list[i] & 0xf)<<4)
print(chr(num^key),end='')

简简单单的解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import base64,urllib.parse
key = "HereIsFlagggg"
flag =

s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
res = []
i = j = 0
for s in flag:
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
k = s_box[t]
res.append(chr(ord(s) ^ k))

cipher = "".join(res)
crypt = (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
enc = str(base64.b64decode(crypt),'utf-8')
enc = urllib.parse.quote(enc)
print(enc)
# enc = %C2%A6n%C2%87Y%1Ag%3F%C2%A01.%C2%9C%C3%B7%C3%8A%02%C3%80%C2%92W%C3%8C%C3%BA

一道python加密题目,urllib.parse这个库用来进行url加密,加密用了quote函数,可以查到解密用unquote函数。主体的s盒加密都是可逆的加加减减异或运算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
res = []
i = j = 0
for s in flag:
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
k = s_box[t]
res.append(chr(ord(s) ^ k))

实际上,真正加密只用了最后一个异或chr(ord(s) ^ k),直接利用加密代码得到移位等操作后的s盒直接对密文异或即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import base64, urllib.parse

key = "HereIsFlagggg"
enc = "%C2%A6n%C2%87Y%1Ag%3F%C2%A01.%C2%9C%C3%B7%C3%8A%02%C3%80%C2%92W%C3%8C%C3%BA"

cipher = urllib.parse.unquote(enc)

s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]

res = []
i = j = 0
for s in cipher:
i = (i + 1) % 256
j = (j + s_box[i]) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
t = (s_box[i] + s_box[j]) % 256
k = s_box[t]
res.append(chr(ord(s) ^ k))
flag = "".join(res)
print(flag)

非常简单的逻辑题

1
2
3
4
5
6
7
8
9
flag = 'xxxxxxxxxxxxxxxxxxxxx'
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
result = ''
for i in range(len(flag)):
s1 = ord(flag[i])//17
s2 = ord(flag[i])%17
result += s[(s1+i)%34]+s[-(s2+i+1)%34]
print(result)
# result = 'v0b9n1nkajz@j0c4jjo3oi1h1i937b395i5y5e0e$i'

又是一道python题,想到两种方法可以做,一种是正着遍历ASCII值得到相同输出得到flag:

1
2
3
4
5
6
7
8
9
10
11
12
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
result = 'v0b9n1nkajz@j0c4jjo3oi1h1i937b395i5y5e0e$i'
flag=''
for i in range(len(result)//2):
res=result[2*i:2*i+2]
for j in range(128):
s1 = j//17
s2 = j%17
temp = s[(s1+i)%34]+s[-(s2+i+1)%34]
if temp==res:
flag+=chr(j)
print(flag)

另一种是根据已知的result反推出原字母,因为构成flag的字符的ASCII值不会出128,所以:
$$
s_1=[\frac j {17}]
$$

$$
s_2=j\bmod 17
$$

$$
j=17s_1+s_2
$$

1
2
3
4
5
6
7
s = 'wesyvbniazxchjko1973652048@$+-&*<>'
result = 'v0b9n1nkajz@j0c4jjo3oi1h1i937b395i5y5e0e$i'
flag=''
for i in range(len(result)//2):
s1=s.find(result[2*i])-i
s2=34-s.find(result[2*i+1])-1-i
print(chr(s1*17+s2),end='')

fakerandom

1
2
3
4
5
6
7
8
9
10
11
12
13
import random
flag = 'xxxxxxxxxxxxxxxxxxxx'
random.seed(1)
l = []
for i in range(4):
l.append(random.getrandbits(8))
result=[]
for i in range(len(l)):
random.seed(l[i])
for n in range(5):
result.append(ord(flag[i*5+n])^random.getrandbits(8))
print(result)
# result = [201, 8, 198, 68, 131, 152, 186, 136, 13, 130, 190, 112, 251, 93, 212, 1, 31, 214, 116, 244]

加密用了随机数,种子都是人工设好的,加密就一个对称的异或,直接按照题目的代码把result和得到的随机数挨个异或即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
import random

result = [201, 8, 198, 68, 131, 152, 186, 136, 13, 130, 190, 112, 251, 93, 212, 1, 31, 214, 116, 244]
flag=''
random.seed(1)
l = []
for i in range(4):
l.append(random.getrandbits(8))
for i in range(len(l)):
random.seed(l[i])
for n in range(5):
flag+=chr(result[i*5+n]^random.getrandbits(8))
print(flag)

fakebase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flag = 'xxxxxxxxxxxxxxxxxxx'

s_box = 'qwertyuiopasdfghjkzxcvb123456#$'
tmp = ''
for i in flag:
tmp += str(bin(ord(i)))[2:].zfill(8)
b1 = int(tmp,2)
s = ''
while b1//31 != 0:
s += s_box[b1%31]
b1 = b1//31

print(s)

# s = u#k4ggia61egegzjuqz12jhfspfkay

在题目代码中,前半部分其实是熟悉的long_to_bytes过程:

1
2
3
4
5
6
7
flag = 'xxxxxxxxxxxxxxxxxxx'

s_box = 'qwertyuiopasdfghjkzxcvb123456#$'
tmp = ''
for i in flag:
tmp += str(bin(ord(i)))[2:].zfill(8)
b1 = int(tmp,2)

所以重点在最后这部分:

1
2
3
4
5
6
7
8
s = ''
while b1//31 != 0:
s += s_box[b1%31]
b1 = b1//31

print(s)

# s = u#k4ggia61egegzjuqz12jhfspfkay

数值b1每一轮都整除31,并在s字符串中加入取模31后的结果,很明显这里需要逆推回去,知道:
$$
s=b_1\bmod 31
$$

$$
b_1=s+31k
$$

k是未知的,但是可以爆破,得知了第一轮的k之后,就可以反复逆推回最初的b1了:

1
2
3
4
5
6
7
8
9
10
from Crypto.Util.number import *

s = 'u#k4ggia61egegzjuqz12jhfspfkay'
s_box = 'qwertyuiopasdfghjkzxcvb123456#$'
flag=''
for k in range(31):
b1=k
for i in s[::-1]:
b1 = b1*31+s_box.index(i)
print(long_to_bytes(int(b1)))

在一堆得到的解码值中能找到flag。

easyapp

一道安卓逆向题,用java编写的,可以使用JADX进行安卓反编译。

打开之后寻找有用的代码,在com文件夹里有个叫MainActivity的,打开,发现:

1
2
3
4
5
6
7
8
9
    public /* synthetic */ void lambda$onCreate$0$MainActivity(final EditText editText, View v) {
System.out.println(encoder.encode(editText.getText().toString()));
if (encoder.encode(editText.getText().toString()).equals("棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌")) {
Toast.makeText(this, "YES", 0).show();
} else {
Toast.makeText(this, "NO", 0).show();
}
}
}

这个方法的函数名为”lambda$onCreate0MainActivity”,它的参数是一个EditText和一个View。该方法首先使用encoder对EditText中的文本进行编码,然后将其与”棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌”进行比较。如果两者相等,则Toast消息将显示为“YES”,否则显示为“NO”。

在旁边的另一个程序里发现了encoder:

1
2
3
4
5
6
7
8
9
10
11
12
/* loaded from: classes.dex */
public class Encoder {
private int key = 123456789;

public String encode(String str) {
StringBuilder sb = new StringBuilder();
for (char c : str.toCharArray()) {
sb.append((char) (c ^ this.key));
}
return sb.toString();
}
}

该方法接受一个字符串作为输入,并返回一个经过简单异或加密后的字符串。在该方法内部,input字符串的每个字符都通过XOR运算与”key”变量进行加密。返回加密后的字符串。

在com里还找到个MainActlvity,它试图访问Encoder类中的”key”变量,并将变量的值设置为987654321:

1
2
3
4
5
6
7
8
9
10
11
public class MainActlvity {
public MainActlvity() {
try {
Field declaredField = Encoder.class.getDeclaredField("key");
declaredField.setAccessible(true);
declaredField.set(MainActivity.encoder, 987654321);
} catch (IllegalAccessException | NoSuchFieldException e) {
e.printStackTrace();
}
}
}

所以,这整个程序逻辑就是个简单的异或,直接异或回去就行了:

1
2
3
4
5
6
code = '棿棢棢棲棥棷棊棐棁棚棨棨棵棢棌'
key = 987654321
flag = ""
for i in code:
flag += chr((ord(i) ^ key) % 128)
print(flag)

SWPUCTF 2022 新生赛

easyre

无壳64位,IDAshift+f12就看到了。看看伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  _main();
printf_0("----------Welcome to NSSCTF Reverse----------");
printf_0("\n");
printf_0("This is the second program of the reverse challenge");
printf_0("\n");
printf_0(aTheFlagIs);
printf_0("\n");
printf_0("oh, you can't see the flag, but you can see the flag");
printf_0("\n");
printf_0("Maybe try to reverse this program?");
printf_0("\n");
printf_0("Good Luck");
printf_0("\n");
printf_0("---------------------------------------------");
v4 = 0;
scanf("%d", &v4);
if ( v4 == 114514 )
printf_0("NSSCTF{oh_you_find_it}");
return 0;
}

base64

无壳64位,IDA打开,一看找到base64加密的密文,还有一个base64字母表,直接解密:

1
2
3
4
import base64

b64='TlNTQ1RGe2Jhc2VfNjRfTlRXUTRaR0ROQzdOfQ=='
print(base64.b64decode(b64))

[BJDCTF 2020]JustRE

exe文件,无壳32位,打开之后找字符串,发现一个长得很像flag的东西:`.data:00407030 aBjdDD2069a4579 db ‘ BJD