dustland

dustball in dustland

xctf攻防世界-reverse-新手村

xctf攻防世界-reverse-新手村

image-20220507171516740

002logmein

main函数

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
main proc near

var_90= qword ptr -90h;很奇怪的是,申请了好多变量,有四字的也有双字的还有字节的
var_88= qword ptr -88h
var_80= qword ptr -80h
var_74= dword ptr -74h
var_70= qword ptr -70h
var_64= dword ptr -64h
var_60= dword ptr -60h
var_5C= dword ptr -5Ch
var_57= byte ptr -57h
var_56= byte ptr -56h
var_55= byte ptr -55h
var_54= dword ptr -54h
s= byte ptr -50h
var_2C= dword ptr -2Ch
var_28= qword ptr -28h
var_20= byte ptr -20h
var_4= dword ptr -4

; __unwind {
;开端:
push rbp
mov rbp, rsp
sub rsp, 90h


mov rdi, offset format ; "Welcome to the RC3 secure password gues"...
mov [rbp+var_4], 0

;一系列蜜汁操作
mov rax, ds:qword_4008B0 ;0x5E54525F4C41223A;
mov qword ptr [rbp+var_20], rax
mov rax, ds:qword_4008B8 ;0x342F362B3F2E2A4C;
mov qword ptr [rbp+var_20+8], rax
mov cx, ds:word_4008C0 ;0x36;
mov word ptr [rbp+var_20+10h], cx

分析一下这里将数据传来传去干了啥

image-20220506112847647

var_20占据了栈上rbp-20到rbp-9共24字节空间

0x5E54525F4C41223A放在栈上var_20,占用四字8字节,

0x342F362B3F2E2A4C放在栈上var_20+8,恰好顺着刚才的var_20存放八个字节

0x36放在栈上var_20+10hvar_20+16也是顺着放的

到此为止,var_20一共使用了0~16共17字节,十六进制的表示为:0x36342F362B3F2E2A4C5E54525F4C41223A

表示成ascii码为:64/6+?.*L^TR_LA":,下面的字符啥也没写即为'\0'结束符号,一定要考虑清楚小端模式

由此可见var_20应当是开在栈上的一个字符数组,大小是24bytes,实际使用17bytes

然后

1
2
mov     rax, qword ptr ds:byte_4008D0			 ;0x65626D61726168;
mov [rbp+var_28], rax

0x65626D61726168一个四字放在栈中var_28上,var_28恰好也是8字节四字长度,表示成8个ascii字符为ebmarah刚好7个字符

然后

1
mov     [rbp+var_2C], 7

var_2C是个双字但是却只放了一个7,可能会与var_28长度7有染

接着下面的反汇编应该调整一下指令顺序,调用_printf或者_scanf函数返回后立刻处理eax中的返回值,看起来比较直观,并且不影响程序逻辑

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
;打印第一句废话
mov rdi, offset format ; "Welcome to the RC3 secure password gues"...
mov ...
call _printf ;printf函数返回值是实际打印字符数
mov [rbp+var_5C], eax
mov al, 0

;打印第二句废话
mov rdi, offset aToContinueYouM ; "To continue, you must enter the correct"...
call _printf
mov [rbp+var_60], eax
mov al, 0

;打印第三句废话
mov rdi, offset aEnterYourGuess ; "Enter your guess: "
call _printf
mov [rbp+var_64], eax
mov al, 0

;调用scanf获取输入
mov rdi, offset a32s ; "%32s"
lea rsi, [rbp+s] ; s作为scanf的缓冲区,获取输入
call ___isoc99_scanf ;scanf返回值是实际获取到的输入字符数
mov [rbp+var_74], eax ;输入字符数->eax

;获取输入长度
lea rdi, [rbp+var_20] ;&var_20->rdi
lea rsi, [rbp+s] ;&s->rsi
mov [rbp+var_70], rdi ;&var_20->rdi->var_70
mov rdi, rsi ;&s->rdi
call _strlen ;strlen(s)->rax
mov [rbp+var_80], rax ;strlen(s)->rax->var_80

;获取存好的字符串长度
mov rdi, [rbp+var_70] ;&var_20->var_70->rdi
call _strlen ;strlen(&var_20)->rax
mov rsi, [rbp+var_80] ;strlen(s)->var_80->rsi

;比较两个长度是否相同
cmp rsi, rax ;比较s和var_20的长度(17)是否一致,即判断是否输入了17个字符
jnb loc_400700 ;如果不一致则跳转报告失败,否则继续检查

下面即将进入循环,现在我们已知的是输入要17个字符

循环体分析

sub_4007c0函数报告失败,不妨给他起名failure

sub_4007F0函数报告成功,不妨给他起名success

image-20220506120601387

进入循环之前有一个var_54=0很自然可以想到的一种循环结构:

1
2
3
4
5
int i=0;
while(true){
...
++i;
}

那么这里var_54是循环变量i吗?

再看最下方的loc_40079E,其中对var_54进行了++,然后跳转loc_400707即循环开头,那么var_54基本上可以确定为循环变量i了

将反汇编翻译为伪代码

image-20220506124657651

解密算法已经很明显了,把var_20var_28都看成字符数组然后decrypt[i]=var_20[i]^var_28[i%7]即可

解密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <algorithm>
using namespace std;

string var_20 = "64/6+?.*L^TR_LA\":";
string var_28 = "ebmarah";

int main() {
reverse(var_20.begin(), var_20.end());//反转一下是因为string字符串最左面的字符是低位,而小端模式下最右边是低位
reverse(var_28.begin(), var_28.end());
for (int i = 0; i < var_20.size(); ++i) {
int temp = (int)var_20[i] ^ (int)var_28[i % 7];
cout << (char)temp;
}
return 0;
}

运行结果:

1
RC3-2016-XORISGUD

003insanity

信息收集

每次运行都根开盲盒似的,等待大约5秒之后给出一个结果

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
┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
#define YOU "massive failure"

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
I've got a good feeling about this one..... wait no. Maybe next time.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
#define YOU "massive failure"

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
rm -rf / : Permission denied

┌──(kali㉿Executor)-[/mnt/c/Users/86135/Desktop/xctf12/insanity]
└─$ ./insanity
Reticulating splines, please wait..
Your ability to hack is about as good as my ability to have free will.

静态分析

一张图截完,题啃腚不难

image-20220507122441995

实际上点进s或者任意一个存好的字符串到rodata区看一下就找到答案了

image-20220507171407247

下面分析一下程序逻辑

猜测是这样的:假设rodata区有n条语句组成一个数组,生成一个随机数然后对n取模,取该值对应下标的语句打印

main函数

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
44
45
46
; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near

argc= dword ptr 8
argv= dword ptr 0Ch
envp= dword ptr 10h

; __unwind {
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov dword ptr [esp], offset s ; "Reticulating splines, please wait.." ;废话压栈
call _puts ;打印废话
mov dword ptr [esp], 5 ; seconds ;5秒压栈
call _sleep ;休眠5秒

;生成随机数种子
mov dword ptr [esp], 0 ; timer ;time(0)
call _time
mov [esp], eax ; seed ;time(0)的返回值作为参数
call _srand ;srand(time(0))

;获得一个随机数
call _rand ;rand()->eax
mov ecx, eax ;rand()->eax->ecx

;下面这一系列操作好迷啊
mov edx, 0CCCCCCCDh ;3435973837这个魔数是啥呢
mul edx ;eax*edx->edx:eax
shr edx, 3 ;edx/8
lea eax, [edx+edx*4] ;5edx->eax
add eax, eax ;10edx->eax
sub ecx, eax ;10edx
;分析了一阵子屁用没有

;取对应字符串并打印
mov eax, strs[ecx*4] ;ecx是下标,相邻亮指针偏移4字节
mov [esp], eax ; s
call _puts
xor eax, eax
leave
retn
; } // starts at 80483F0
main endp
image-20220507162916682

全篇没有看出一个取模来,难道是我老眼昏花了?

取模时的编译优化

这里应该就是取模,但是其实现好迷啊

1
2
3
4
5
6
7
8
9
10
call    _rand																;rand()->eax假设为16
mov ecx, eax ;rand()=16->eax->ecx

;下面这一系列操作好迷啊
mov edx, 0CCCCCCCDh ;3435973837这个魔数是啥呢
mul edx ;eax*edx->edx:eax=Ch:CCCCCCD0h
shr edx, 3 ;edx/8=C/8=1
lea eax, [edx+edx*4] ; 5->eax
add eax, eax ; 10->eax
sub ecx, eax ; 16-10=6

(5条消息) 逆向-取模运算_嘻嘻兮的博客-CSDN博客_取模的逆运算

image-20220507170255414

006re1

信息收集

1
2
3
4
5
6
PS C:\Users\86135\Desktop\xctf12\re1> ./re1   
欢迎来到DUTCTF呦
这是一道很可爱很简单的逆向题呦
输入flag吧:123456
flag不太对呦,再试试呗,加油呦
请按任意键继续. . .

输入flag之后必然会flag不对,然后程序阻塞,并且提示"请按任意键继续..."

这个题的图视图竟然可以用一个截屏截下来,必然简单

image-20220507101716875

逆向分析

开端

1
2
3
4
5
;开端
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 44h

防止栈缓冲区溢出

1
2
3
.text:00401006                 mov     eax, ___security_cookie
.text:0040100B xor eax, ebp
.text:0040100D mov [ebp+var_4], eax

装填flag,指令顺序有改动但是不影响逻辑

1
2
3
4
5
6
7
8
.text:00401018                 xor     eax, eax					;eax归零
.text:00401010 movdqu xmm0, ds:xmmword_413E34 ;3074656D30633165577B465443545544h
;即 0tem0c1eW{FTCTUD
.text:0040101F movdqu [ebp+var_44], xmm0 ;0tem0c1eW{FTCTUD->var_44
.text:00401024 mov [ebp+var_2C], eax ;eax=0->var_2C
.text:00401027 movq xmm0, ds:qword_413E44 ;7D465443545544h即 }FTCTUD
.text:0040102F movq [ebp+var_34], xmm0 ;}FTCTUD ->var_34
.text:00401034 mov [ebp+var_28], ax ;0->var_28

打印废话

1
2
3
4
5
6
.text:0040101A                 push    offset aDutctf  ; "欢迎来到DUTCTF呦\n"	;废话压栈作为参数
.text:00401038 call _printf ;打印第一句废话
.text:0040103D push offset asc_413E60 ; "这是一道很可爱很简单的逆向题呦\n"
.text:00401042 call _printf
.text:00401047 push offset aFlag ; "输入flag吧:"
.text:0040104C call _printf

获取输入,输入存储到var_24

1
2
3
4
.text:00401051                 lea     eax, [ebp+var_24]
.text:00401054 push eax
.text:00401055 push offset Format ; "%s"
.text:0040105A call _scanf

加载输入和flag

1
2
3
.text:0040105F                 add     esp, 14h
.text:00401062 lea eax, [ebp+var_24] ;输入字符串的地址
.text:00401065 lea ecx, [ebp+var_44] ;flag的地址

循环比较输入和flag,每次比较两个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00401068 loc_401068:                             ; CODE XREF: _main+82↓j
.text:00401068 mov dl, [ecx] ;flag[0]值放在dl
.text:0040106A cmp dl, [eax] ;比较flag[0]和输入值的大小
.text:0040106C jnz short loc_401088 ;如果不为零即输入和flag不相等则跳转,大概率跳转到寄
.text:0040106E test dl, dl ;dl中存放的是flag[i]如果为0说明遍历完毕,循环跳出
.text:00401070 jz short loc_401084 ;如果遍历完毕则跳转loc_401084
.text:00401072 mov dl, [ecx+1]
.text:00401075 cmp dl, [eax+1]
.text:00401078 jnz short loc_401088
.text:0040107A add ecx, 2
.text:0040107D add eax, 2
.text:00401080 test dl, dl
.text:00401082 jnz short loc_401068

两种情况,置eax为0或1然后合并判断

1
2
3
.text:00401084 loc_401084:                             ; CODE XREF: _main+70↑j
.text:00401084 xor eax, eax ;eax归零
.text:00401086 jmp short loc_40108D ;跳转loc_401108D
1
2
3
4
.text:00401088 loc_401088:                             ; CODE XREF: _main+6C↑j
.text:00401088 ; _main+78↑j
.text:00401088 sbb eax, eax ;带借位减法,eax=-1=0xFFFFFFFF
.text:0040108A or eax, 1 ;eax=1&0xFFFFFFF=1

合并.根据刚才的eax判断

1
2
3
4
5
6
7
8
.text:0040108D loc_40108D:                             ; CODE XREF: _main+86↑j
.text:0040108D test eax, eax ;判断eax是否为0
.text:0040108F jnz short loc_401098 ;如果eax!=0则跳转loc_401098
.text:00401091 push offset unk_413E90;flag get ;否则eax=0表示成功,"flag get"的地址压栈准备打印
.text:00401096 jmp short loc_40109D ;跳转loc_40109D
.text:00401098 loc_401098: ; CODE XREF: _main+8F↑j
.text:00401098 push offset aFlag_0 ; "flag不太对呦" ;"flag不太对呦"地址压栈,准备打印

尾声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0040109D loc_40109D:                             ; CODE XREF: _main+96↑j
.text:0040109D call _printf ;打印刚才压入栈中的缓冲区
.text:004010A2 add esp, 4 ;退栈4
.text:004010A5 push offset Command ; "pause" ;"pause"串的地址压栈,准备为system函数传参
.text:004010AA call _system ;程序阻塞暂停

;以下部分可能是在检查栈缓冲区溢出?
.text:004010AF mov ecx, [ebp+var_4]
.text:004010B2 add esp, 4
.text:004010B5 xor ecx, ebp ; StackCookie
.text:004010B7 xor eax, eax
.text:004010B9 call @__security_check_cookie@4 ; __security_check_cookie(x)
.text:004010BE mov esp, ebp
.text:004010C0 pop ebp
.text:004010C1 retn
.text:004010C1 _main endp

参考微软官方文档

全局安全 Cookie 用于在使用 /GS(缓冲区安全检查)编译的代码中和使用异常处理的代码中提供缓冲区溢出保护。 进入受到溢出保护的函数时,Cookie 被置于堆栈之上;退出时,会将堆栈上的值与全局 Cookie 进行比较。 它们之间存在任何差异则表示已经发生缓冲区溢出,并导致该程序的立即终止。

通常, __security_init_cookie 在初始化时由 CRT 调用。 如果你跳过 CRT 初始化(例如,如果使用 /ENTRY 指定入口点),则必须自己调用 __security_init_cookie 。 如果 __security_init_cookie 不调用,全局安全 cookie 将设置为默认值,并且会危及缓冲区溢出保护。 由于攻击者可利用此默认 Cookie 值使缓冲区溢出检查无效,我们建议,在定义自己的入口点时,始终调用 __security_init_cookie

在进入任何受到溢出保护的函数前,必须调用 __security_init_cookie,否则将检测到虚假的缓冲区溢出。 有关详细信息,请参阅 C 运行时错误 R6035

007game

信息收集

一个32位exe程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                     |------------/ --------△--------|
|-----------------------●--------|
|------------/ --------◇--------|
|-----------------------■--------|
|--------------------|------------/ --------☆--------|
| |------------/ --------▽--------|
| |------------/ -----( ̄▽ ̄)/---|
| |--------------------(*°▽°)=3--|
二 |
| by 0x61 |
| |
|------------------------------------------------------|
input n,n(1-8)
1.△ 2.○ 3.◇ 4.□ 5.☆ 6.▽ 7.( ̄▽ ̄)/ 8.(;°Д°) 0.restart
n=

一个开灯和关灯的游戏,大意是:

有八栈灯编号为1到8,每次可以选择一盏灯改变其开关状态,并且其左右的灯也会改变开关状态,认为8号灯和1号灯也是相邻的

初始时八盏灯都是灭的

当八盏灯都亮起来的时候,就给flag

算法解决

首先考虑这个问题是否有解?

方便取模运算,将灯号从0编到7

灯号 0 1 2 3 4 5 6 7
初始 0 0 0 0 0 0 0 0
第一次 1 ==1== 1 0 0 0 0 0
第二次 1 0 ==0== 1 0 0 0 0
第三次 1 0 0 0 ==1== 1 0 0
第四次 1 0 0 0 0 ==0== 1 0
第五次 0 0 0 0 0 0 0 ==1==

经过五次变化之后,我们可以得到一栈两着的灯,并且其他暗着的灯我们可以视为一直暗着

==因此同时改变三栈灯的状态是可以推出等价于只改变一盏灯的状态==

从1号灯开始的变化导致了7号灯变化

同理可知从2号灯开始的变化导致0号灯变化

...

从i号灯开始的变化导致(i+6)%8的灯变化,其路径为i%8,(i+1)%8,(i+3)%8,(i+4)%8,(i+6)%8共五次变化

则8盏灯全部由暗变亮需要40次变化

可以编写解密脚本打印出这40个数字然后粘贴进game.exe解密

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <cstdio>
using namespace std;

void change(const int &i) {
cout << i % 8 + 1 << endl;//实际上还得是从1开始编号
cout << (i + 1) % 8 + 1 << endl;
cout << (i + 3) % 8 + 1 << endl;
cout << (i + 4) % 8 + 1 << endl;
cout << (i + 6) % 8 + 1 << endl;
}

int main() {
for (int i = 0; i < 8; ++i) {
change(i);
}
return 0;
}

结果粘贴进game.exe之后

1
done!!! the flag is zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}

静态分析

尝试1

假设我们不会玩这个游戏,但是可以猜测,这⑧盏灯是可以都点亮的,诚如是,则程序交出flag,退出

那么在反汇编图视图中应当有一些没有后继的叶子块,即成功的分支

满足条件的块只有一个,如图所示红色块

image-20220506234409960

但是这个块实际上干了和栈有关一些事,具体啥事也不想管了,总之这样猜测是错的

刚才用算法方法点亮了所有的灯,程序也给出了flag但是程序依然在执行

尝试2

每次输入改变灯状态之后会有一个对所有灯的状态检查,要么是在循环开始,要么是在循环尾部

这八盏灯的状态可能是在放在一个字节里的八位,通过按位操作改变状态

或者每个灯一个单元,放在一个数组里

观察反汇编的图视图,发现有这么八块基本相同的结构,大概率就是逐次判断灯的状态

image-20220507075557175

观察其中的一块

1
2
3
4
5
mov     eax, 1					;1->eax
shl eax, 0
movzx ecx, byte_532E28[eax] ;byte_532E28[1]->ecx
cmp ecx, 1 ;cmp byte_532E28[1],1
jnz short loc_45F671 ;不为0则表明灯灭,跳转从头再来

那么如果八块均不跳转jnz,那么紧接着就应该给出flag了

image-20220507080122539

结果该函数开头给出了好长一坨字符数组,然后给了这么一个循环,明显是有加密算法的

image-20220507081140177

蓝色块用作打印,和加密算法无关,暂时不关心

结尾loc_45EB61将var_94++,显然是作为循环变量的,

开头loc_45EB70var_940x38h=56进行比较,应该是判断循环结束条件,循环56次

loc_45EB70之前有一个mov [ebp+var_94],0显然是循环变量var_94一开始是下标0

那么不妨给var_94重命名i

循环体:

1
2
3
4
5
6
7
8
9
10
11
12
13
mov     eax, [ebp+i]
movsx ecx, [ebp+eax+var_44] ;var_44[i]->ecx
mov edx, [ebp+i]
movsx eax, [ebp+edx+var_88] ;var_88[i]->eax
xor eax, ecx ;var_44[i]^var_88[i]->eax
mov ecx, [ebp+i]
mov [ebp+ecx+var_88], al ;var_44[i]^var_88[i]->var_88[i]
mov eax, [ebp+i]
movsx ecx, [ebp+eax+var_88] ;var_88[i]->ecx
xor ecx, 13h ;var_88[i]^13h->ecx
mov edx, [ebp+i]
mov [ebp+edx+var_88], cl ;var_88[i]^13h->var_88[i]
jmp short loc_45EB61

这么一长段干了一件事 \[ var\_88[i]=var\_88[i]\oplus var\_44[i]\oplus 0x13 \] var_88在内存中占用-88到-50刚好0x38大小,与循环变量i的范围相同

同理var_44应当占用-44-C

在栈视图下编好数组

image-20220507083640431

回到反汇编视图

接下来考虑怎样把mov进入var_44数组的值提取出来放到一个数组里

image-20220507084508649

如果高亮.text:0045E975.text:0045EA55之后右键convert to C/C++ array(DWORD)

image-20220507084855688

得到的数组是这样的:

1
2
3
4
5
6
7
8
9
10
[+] Dump 0x45E975 - 0x45EA55 (224 bytes) :
unsigned int data[56] = {
0x12BC45C6, 0x40BD45C6, 0x62BE45C6, 0x05BF45C6, 0x02C045C6, 0x04C145C6, 0x06C245C6, 0x03C345C6,
0x06C445C6, 0x30C545C6, 0x31C645C6, 0x41C745C6, 0x20C845C6, 0x0CC945C6, 0x30CA45C6, 0x41CB45C6,
0x1FCC45C6, 0x4ECD45C6, 0x3ECE45C6, 0x20CF45C6, 0x31D045C6, 0x20D145C6, 0x01D245C6, 0x39D345C6,
0x60D445C6, 0x03D545C6, 0x15D645C6, 0x09D745C6, 0x04D845C6, 0x3ED945C6, 0x03DA45C6, 0x05DB45C6,
0x04DC45C6, 0x01DD45C6, 0x02DE45C6, 0x03DF45C6, 0x2CE045C6, 0x41E145C6, 0x4EE245C6, 0x20E345C6,
0x10E445C6, 0x61E545C6, 0x36E645C6, 0x10E745C6, 0x2CE845C6, 0x34E945C6, 0x20EA45C6, 0x40EB45C6,
0x59EC45C6, 0x2DED45C6, 0x20EE45C6, 0x41EF45C6, 0x0FF045C6, 0x22F145C6, 0x12F245C6, 0x10F345C6
};

实际上存储的是指令不是我们要的数值

但是可以发现,每个数据的高两个16进制位是我们要的数值

比如0x12BC45C6高两位那么将每个16进制数按位与0xFF000000即可只保留高两位,或者直接右移24位即可转化为低两位

但是对于var_88这个数组,它头几个元素赋值的指令占了7字节,可以手动抄到数组里

image-20220507090007954
1
2
3
4
5
6
7
8
9
unsigned int var_88_2[56] = {
0x7b000000, 0x20000000, 0x12000000, 0x62000000, 0x77000000, 0x6C000000, 0x41000000, 0x29000000,
0x7C8045C6, 0x508145C6, 0x7D8245C6, 0x268345C6, 0x7C8445C6, 0x6F8545C6, 0x4A8645C6, 0x318745C6,
0x538845C6, 0x6C8945C6, 0x5E8A45C6, 0x6C8B45C6, 0x548C45C6, 0x068D45C6, 0x608E45C6, 0x538F45C6,
0x2C9045C6, 0x799145C6, 0x689245C6, 0x6E9345C6, 0x209445C6, 0x5F9545C6, 0x759645C6, 0x659745C6,
0x639845C6, 0x7B9945C6, 0x7F9A45C6, 0x779B45C6, 0x609C45C6, 0x309D45C6, 0x6B9E45C6, 0x479F45C6,
0x5CA045C6, 0x1DA145C6, 0x51A245C6, 0x6BA345C6, 0x5AA445C6, 0x55A545C6, 0x40A645C6, 0x0CA745C6,
0x2BA845C6, 0x4CA945C6, 0x56AA45C6, 0x0DAB45C6, 0x72AC45C6, 0x01AD45C6, 0x75AE45C6, 0x7EAF45C6
};

解密脚本

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
44
45
#include <iostream>
#include <cstdio>
#include <vector>
using namespace std;

unsigned int var_44[56] = {
0x12BC45C6, 0x40BD45C6, 0x62BE45C6, 0x05BF45C6, 0x02C045C6, 0x04C145C6, 0x06C245C6, 0x03C345C6,
0x06C445C6, 0x30C545C6, 0x31C645C6, 0x41C745C6, 0x20C845C6, 0x0CC945C6, 0x30CA45C6, 0x41CB45C6,
0x1FCC45C6, 0x4ECD45C6, 0x3ECE45C6, 0x20CF45C6, 0x31D045C6, 0x20D145C6, 0x01D245C6, 0x39D345C6,
0x60D445C6, 0x03D545C6, 0x15D645C6, 0x09D745C6, 0x04D845C6, 0x3ED945C6, 0x03DA45C6, 0x05DB45C6,
0x04DC45C6, 0x01DD45C6, 0x02DE45C6, 0x03DF45C6, 0x2CE045C6, 0x41E145C6, 0x4EE245C6, 0x20E345C6,
0x10E445C6, 0x61E545C6, 0x36E645C6, 0x10E745C6, 0x2CE845C6, 0x34E945C6, 0x20EA45C6, 0x40EB45C6,
0x59EC45C6, 0x2DED45C6, 0x20EE45C6, 0x41EF45C6, 0x0FF045C6, 0x22F145C6, 0x12F245C6, 0x10F345C6
};

unsigned int var_88[56] = {
0x7b000000, 0x20000000, 0x12000000, 0x62000000, 0x77000000, 0x6C000000, 0x41000000, 0x29000000,//手动补的
0x7C8045C6, 0x508145C6, 0x7D8245C6, 0x268345C6, 0x7C8445C6, 0x6F8545C6, 0x4A8645C6, 0x318745C6,
0x538845C6, 0x6C8945C6, 0x5E8A45C6, 0x6C8B45C6, 0x548C45C6, 0x068D45C6, 0x608E45C6, 0x538F45C6,
0x2C9045C6, 0x799145C6, 0x689245C6, 0x6E9345C6, 0x209445C6, 0x5F9545C6, 0x759645C6, 0x659745C6,
0x639845C6, 0x7B9945C6, 0x7F9A45C6, 0x779B45C6, 0x609C45C6, 0x309D45C6, 0x6B9E45C6, 0x479F45C6,
0x5CA045C6, 0x1DA145C6, 0x51A245C6, 0x6BA345C6, 0x5AA445C6, 0x55A545C6, 0x40A645C6, 0x0CA745C6,
0x2BA845C6, 0x4CA945C6, 0x56AA45C6, 0x0DAB45C6, 0x72AC45C6, 0x01AD45C6, 0x75AE45C6, 0x7EAF45C6
};


vector<char> preprocesser(const unsigned int arr[], int length) {
vector<char> v(length);
for (int i = 0; i < length; ++i) {
v[i] = arr[i] >> 24;
}
return v;
}


int main() {
vector<char> result = preprocesser(var_44, 56);
vector<char> key = preprocesser(var_88, 56);
for (int i = 0; i < 56; ++i) {
result[i] = result[i] ^ key[i] ^ 0x13;
cout << (char)result[i];
}

return 0;
}

运行结果:

1
zsctf{T9is_tOpic_1s_v5ry_int7resting_b6t_others_are_n0t}

008Helloc,CTF

没见过的指令

在分析之前,补几个指令

关于CSAPP上没有见过的指令

1.rep/repne

2.movsd/movsw/movsb

1
2
3
rep movsd               ; 没有见过的指令
movsw
movsb

查阅x86 - Assembly: REP MOVS mechanism - Stack Overflow

According to MSDN, "The instruction can be prefixed by REP to repeat the operation the number of times specified by the ecx register."

rep的操作数是一个指令,rep的作用是将该指令重复若干次,以ecx中的数字为重复次数,(每次ecx中的数字-1直到归零)

repne 当ecx!=0且ZF!=0,重复执行后边的指令,每执行一次ecx的值减1

image-20220506171358295

3.stosd/stosw/stosb

1
2
3
rep stosd
stosw
stosb

STOSB、STOSW 和 STOSD 指令分别将 AL/AX/EAX 的内容存入由 EDI 中偏移量指向的内存位置。

EDI 根据方向标志位的状态递增或递减。

4.scasd/scasw/scasb

SCASB、SCASW 和 SCASD 指令分别将 AL/AX/EAX 中的值与 EDI 寻址的一个字节 / 字 / 双字进行比较。

这些指令可用于在字符串或数组中寻找一个数值。

结合 REPE(或 REPZ)前缀,当 ECX > 0 且 AL/AX/EAX 的值等于内存中每个连续的值时,不断扫描字符串或数组。

收集信息

输入比较短的字符串会报告wrong!然后让重新输入,输入很长的字符串报告wrong!之后程序退出

image-20220506163947900

ida打开之后

main函数开端部分

1
2
3
4
...
mov esi, offset a437261636b4d65 ; "437261636b4d654a757374466f7246756e"
lea edi, [esp+70h+var_24]
...

这里有一串16进制编码,其他不管先解一下,CrackMeJustForFun"撬我只是为了娱乐",看上去是有一定语言意义的,作为flag交上去试试就对了

反汇编分析

下面分析一下程序都干了啥

开端

1
2
3
4
5
6
7
8
9
10
11
12
13
;开端
sub esp, 60h
mov ecx, 8
push ebx;寄存器值保存
push ebp
push esi
push edi

mov esi, offset a437261636b4d65 ; esi存放flag的地址
lea edi, [esp+70h+var_24] ; &var_24->edi
rep movsd ;ecx事先放好了8,这里重复执行movsd八次
movsw
movsb

这里

1
2
3
rep movsd  
movsw
movsb

三句话,让我蒙蔽了18分钟,我真是一个计组学的一塌糊涂的虚蛋

参考博客标志寄存器df_标志寄存器_shikaao14的博客-CSDN博客

这里干了一个字符串拷贝的事情,offset a437261636b4d65这个位置的字符串作为源,esp+70h+var_24这个位置的缓冲区作为目的进行拷贝.

为什么用到了三条指令?

源串:437261636b4d654a757374466f7246756e共占用了35字节 \[ 35\div 4=8··\ ·3 \] 首先rep movsd重复8次,每次拷贝4字节,一共拷贝了32字节,剩下了3个字节,拆成一个字用movsw然后最后一个字节用movsb

上述部分做的就是拷贝.data上的字符串到栈上var_24开始的35个字节

下面进入循环体

image-20220506181931623

外循环(外侧蓝线)

这个循环体的结构很容易猜想,收集信息时我们知道如果输入字符数比较少则程序会一直重复运行,不存在循环变量,终止条件之类.因此循环体完全可以按照单独一次执行进行饭分析

loc_40101A

这是循环体的起始部分

一些参数及其函数调用距离太远不直观,这里调整了一些语句位置,但是不改变程序逻辑

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
loc_40101A:
mov ecx, 8 ;rep的帮凶
xor eax, eax ;eax归零
lea edi, [esp+70h+var_48] ; edi指向了&var_48
rep stosd ;STOSB、STOSW 和 STOSD 指令分别将 AL/AX/EAX 的内容存入由 EDI 中偏移量指向的内存位置。
stosw
stosb
;eax被xor指令清零了,那么这里三条指令的作用是让var_48开始的35字节的栈空间置0

;打印第一句废话
push offset aPleaseInputYou ; "please input your serial:"
call _printf

;准备获取用户输入
lea eax, [esp+74h+var_5C] ;eax指向&var_5C
push eax ;&var_5C压栈作为scanf的缓冲区
push offset aS ; "%s"压栈作为第一个参数
call _scanf

;检查是否给了太多字符
lea edi, [esp+7Ch+var_5C] ; &var_5C->edi
or ecx, 0FFFFFFFFh ; ecx置全1
xor eax, eax ;eax归零,如果eax为0则ZF置零,eax存放scanf返回值,即实际输入字符数
add esp, 0Ch ;退栈12字节,不会修改ZF位
repne scasb ;ecx中全是1,相当于无限大,只要scanf有输入则repne成立;从var_5C指向的缓冲区中寻找0(eax中放的是0),每次ecx-1
not ecx ;ecx取反
dec ecx ;ecx-1
cmp ecx, 11h ;检查ecx和11h=17的大小

ja loc_40110D ;跳转则寄

xor ebx, ebx ;ebx寄存器归零,在loc_40101A的结尾干了这么一件事,不明觉厉,实际上是为后来的内圈循环初始化循环变量

检查输入字符数是怎样实现的?

1.第25行的scasb从edi指向的缓冲区检查,是否存在eax中的字符,因此之前(第21行)就安置好了edilea edi, [esp+7Ch+var_5C]

2.22行ecx置全1,需要减2^8次才能归零,因此该条件对第25行的repne指令没有限制作用

3.23行eax中是scanf的返回值,根据其值置ZF位(假设scanf获取到了输入,则eax不为0,则ZF为0),然后eax归零

4.24行退栈啥作用不知道,推测一开始预先多开了一些栈空间,可能是防止栈缓冲区溢出

5.25行repne成立的条件是ecx!=0,ZF!=1显然当scanf有获取到输入时是成立的,重复执行scasb,即在edi指向的&var_5C中寻找eax中存放的值0,即寻找var_5C的结束字符,每次ecx-1

6.26行ecx取反,然后第27行ecx-1,啥作用呢?

假设输入了ABC就三个字符,scanf返回后eax存3,var_5C={'A','B','C',0,0,...,0},

此时repne条件成立,在第四次检查时检查到0,目前的值为0xFFFFFFFB

然后ecx取反得到0b0100=4

然后ecx-1=3即输入字符数

即这两步执行完后ecx存放输入字符数

7.28行ecx和11h进行比较,如果ecx中的值大则跳转loc_40110D,即判断输入字符是否超过了11h=17个,

而我们一开始收集到的信息也是字符数很多时直接程序退出

8.loc_40110D作用是报告失败

loc_40101A的作用就是获取输入并检查输入长度是否超过17字节,超过则寄,没超过则进一步检查

loc_40105F(内侧绿线)

在研究该部分之前,先要弄明白sprintf函数

image-20220506184650282
1
int sprintf(char *str, const char *format, ...)

返回值是实际str接收到的字符数,将源缓冲区从基地址到'\0'以某种格式输出到str目的缓冲区

内圈循环

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
44
45
46
47
48
49
50
51
52
53
54
55
loc_40105F:
mov al, [esp+ebx+70h+var_5C] ;在进入循环体之前,loc_40101A最的最后,ebx置零,这里又和var_5C结合使用,可以大胆推测这是一个基址变址寻址,基址是esp+70h+var_5c即&var_5C,变址即偏移量ebx,以后都记作i
test al, al
jz short loc_4010B0 ;如果var_5c[i]为0即i遍历到var_5c字符串的末尾则跳转loc_4010B0



;如果jz条件跳转未实现则var_5C[i]!=0即不是字符串末尾
;以下将var_5C处的字符串转化成16进制格式的字符串然后放到var_48位置

;将var_5c[i]转化成16进制字符串,放到buffer
movsx ecx, al ;var_5c[i]->ecx ;字节拓展双字
push ecx ;var_5c[i]->ecx->压栈作为参数
lea edx, [esp+74h+Buffer] ;&Buffer->edx, ;Buffer是栈上两个字节
push offset Format ; "%x" ;%x压栈作为参数
push edx ; Buffer ;&Buffer->edx->压栈作为参数
call _sprintf ;sprintf(&Buffer,"%x",&var_5c[i]),返回值(写入字符总数)放eax
;这里sprintf函数把var_5c[i]以十六进制字符格式转换成字符串放到Buffer
;(Buffer两个字节,一个ascii码的16进制字符串也是两个字节)


lea edi, [esp+7Ch+Buffer] ;&Buffer->edi
or ecx, 0FFFFFFFFh ;ecx置全1
xor eax, eax ;根据eax置ZF,然后eax置零
add esp, 0Ch
repne scasb ;寻找buffer的结束位置
not ecx ;ecx存放buffer的结束字符下标
sub edi, ecx ;edi-ecx之后edi回退到Buffer起始位置


;下面加载var_48并确定其末尾位置
lea edx, [esp+70h+var_48] ;&var_48->edx, ;var_48是栈上一个32字节的缓冲区
mov ebp, ecx ;ebp暂时保存之前的ecx
or ecx, 0FFFFFFFFh

mov esi, edi ;edi存放Buffer基地址,放到esi
mov edi, edx ;edx存放var_48基地址,放到edi

repne scasb ;寻找edi中var_48串的结束字符位置
dec edi ;减1后edi指向var_48的最后一个字符

;这里看似是蜜汁操作,啥也没干,但是我感觉是因为没有用编译优化,编译器在将Buffer向var_48拷贝的时候,没有考虑Buffer的长度,而是假设Buffer是一个不知长度的字符串,先以最大效率双字拷贝,然后最后剩下的不足双字的1或者2或者3个字节使用字节拷贝
;这蜜汁操作想了一下午才想明白
mov ecx, ebp ;ebp把之前暂时保存的值还给ecx
shr ecx, 2 ;ecx之前存放buffer结束字符下标,现在/4必然是0
rep movsd ;按双字拷贝,但是Buffer长度/4=0,因此实际上这三句话啥也没干

mov ecx, ebp ;ebp把之前暂时保存的值还给ecx
and ecx, 3 ;ecx保留低2位(模4余数)
rep movsb ;Buffer按字节拷贝到var_48


inc ebx ;++i;
cmp ebx, 11h ;判断i是否遍历完了var_5C
jl short loc_40105F ;如果i<17则跳到本部分开头重新内圈循环,如果i>=17则意味着遍历完毕,跳出内圈循环

内圈循环做的就是将var_5c存放的串换成16进制格式,输出到var_48位置的串,以Buffer作为转换过渡

var_5c[i]--sprintf-->Buffer--strcat-->var_48

loc_4040B0
1
2
3
loc_4010B0:	
lea esi, [esp+70h+var_24] ;var_24事先放好了flag的16进制表示 &var_24->esi
lea eax, [esp+70h+var_48] ;var_48是刚才内圈循环时将输入转换成16进制串的表示 &var_28->eax

两个16进制数都放好了,可想而知下面要对两个数比较判断是否相同了,显然直接调用strcmp这种函数太过于明显,可能会采用逐个字节比较的方式

但是这样也比较容易发现,

当比较完了*var_24*var_48var_24[0]var_48[0]之后,如果要继续比较var_24[1]var_48[1],需要移动一下指针,即

1
2
inc esi;
inc eax;

这类的指令

loc_4010B8

按照刚才的猜想,这里应该有一个循环,实际上也是这样的

image-20220506205727285

我们很容易就在.text:004010D2找到了移动指针的语句,只不过是一次性移动两位,因为前面比较了两位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:004010B8 loc_4010B8:                             ; CODE XREF: _main+DA↓j
.text:004010B8 mov dl, [eax] ;解引用
.text:004010BA mov bl, [esi]
.text:004010BC mov cl, dl
.text:004010BE cmp dl, bl
.text:004010C0 jnz short loc_4010E0 ;这个跳转通向 寄 ,如果不让他跳则dl和bl要相同,即两个串对应字符相同
.text:004010C2 test cl, cl ;如果cl是0说明指针已经移动到空了,应当跳出循环
.text:004010C4 jz short loc_4010DC ;跳出循环,通向 成功
.text:004010C6 mov dl, [eax+1] ;一次性比较两个字符
.text:004010C9 mov bl, [esi+1]
.text:004010CC mov cl, dl
.text:004010CE cmp dl, bl
.text:004010D0 jnz short loc_4010E0 ;跳转就寄
.text:004010D2 add eax, 2 ;移动指针
.text:004010D5 add esi, 2
.text:004010D8 test cl, cl
.text:004010DA jnz short loc_4010B8 ;本次两个字符都相同,继续循环判断下两个字符是否相同
尾声

从刚才的循环出来就能够决定命运了,刚才的循环有两种跳转,一是loc_4010DC,另一个是loc_4010E0

其中loc_4010DC将eax置零

loc_4010E0将eax置-1

然后两路殊途同归到loc_4010E5

loc_4010E5是一个法官,只有eax值为0才让打印成功

如果eax值不为0则跳转loc_4010FB

image-20220506205845976

009opensource

给出的是一段c程序

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
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
if (argc != 4) {//要求argc=4,argv[0]是自带的程序位置,剩下即要输入3个参数
printf("what?\n");
exit(1);
}

unsigned int first = atoi(argv[1]);//第一个参数转化成字符串
if (first != 0xcafe) {//要求first=51966
printf("you are wrong, sorry.\n");
exit(2);
}

unsigned int second = atoi(argv[2]);//second为第二个参数转化成字符串
if (second % 5 == 3 || second % 17 != 8) {//要求second 模5不余三,模17余8
printf("ha, you won't get it!\n");
exit(3);
}

if (strcmp("h4cky0u", argv[3])) {//要求argv[3]="h4cky0u",长度为7
printf("so close, dude!\n");
exit(4);
}

printf("Brr wrrr grr\n");
//second%17=8,strlen(argv[3])=7
unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;

printf("Get your key: ");
printf("%x\n", hash);
return 0;
}

三个参数可以为:

1
51966 25  h4cky0u

编译之后运行一下:

1
2
3
PS C:\Users\86135\Desktop\xctf12\opensource> ./opensource 51966 25  h4cky0u
Brr wrrr grr
Get your key: c0ffee

010no-strings-attached

尽量不依赖工具,比如ida-F5的伪代码,那么应该在以什么为基础进行分析呢?

objdump和ida的反汇编视图起码是可以的,没有必要和0和1组成的二进制码打交道,直接看反汇编即可

ida反汇编的图视图也是可以接受的,毕竟有了反汇编之后我们也可以轻松画出跳转关系和调用关系图,不妨让ida代劳了

ida-F5伪代码可以吗?

如果我能够轻松地将反汇编翻译成伪代码或者代码,那何乐而不为?但是我没这本事.

如果每个题上来就看伪代码对于从反汇编到伪代码的翻译是没有帮助的.

因此我觉得逆向分析应当基于反汇编和图视图,尽量少沉迷美色看伪代码

对于no-strings-attached这个ctf逆向题,前置知识:

x86汇编语言(如果不看伪代码的话)

==宽字符==

置换加密算法

ida的使用

main函数

main函数下调用了四个函数

image-20220504202713613

_setlocale是使用者的区域设定,比如时间,语言之类的,没有卵用

banner,prompt_authentication都是打印一些语句作为提示,也没有锤子用

authenticate关键在这里

authenticate函数

粗略浏览该函数,发现有一个call decrypt

image-20220504202933305

单就从decrypt,这种"解密"名字的函数就应该是关键函数,authenticate的其他部分先不管,直接看decrypt,

another(rename之后的名字)和s分别作为第二个和第一个参数传递给了decrypt函数

decrypt函数

image-20220504203108886

框框相连,转转不已,也不知道套了多少层循环了

首先按照CSAPP第三章的方法,将汇编翻译成带goto的c伪代码

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
char *decrypt(char *s,char *another){
int slength=strlen(s);
int anotherlength=strlen(another);
char *dest=(int *)malloc(slength+1);
strcpy(dest,s);
goto loc_80486F7;
loc_80486AF:
int var_18=0;
goto loc_80486E7;

loc_80486B8:
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;

loc_80486E7://这一块的分析是比较繁琐的
if(var_18<anotherlength&&var_1C<slength){
goto loc_80486B8;
}

loc_80486F7:
if(var_1C>=slength){
return dest;
}
else{
goto loc_80486AF;
}
}

loc_80486E7的分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:080486B8 loc_80486B8:                            ; CODE XREF: decrypt+9D↓j
.text:080486B8 mov eax, [ebp+var_1C]
.text:080486BB shl eax, 2
.text:080486BE add eax, [ebp+dest] ;dest+4*var_1C->eax
.text:080486C1 mov edx, [ebp+var_1C]
.text:080486C4 shl edx, 2
.text:080486C7 add edx, [ebp+dest] ;dest+4*var_1C->edx
.text:080486CA mov ecx, [edx] ;[dest+4*var_1C]->ecx
.text:080486CC mov edx, [ebp+var_18]
.text:080486CF shl edx, 2
.text:080486D2 add edx, [ebp+another] ;another+4*var_18->edx
.text:080486D5 mov edx, [edx] ;[another+4*var_18]->edx
.text:080486D7 mov ebx, ecx ;[dest+4*var_1C]->ebx
.text:080486D9 sub ebx, edx ;[dest+4*var_1C]-[another+4*var_18]->ebx
.text:080486DB mov edx, ebx ;[dest+4*var_1C]-[another+4*var_18]->ebx->dex
.text:080486DD mov [eax], edx ;[dest+4*var_1C]=[dest+4*var_1C]-[another+4*var_18]
.text:080486DF add [ebp+var_1C], 1 ;++var_1C
.text:080486E3 add [ebp+var_18], 1 ;++var_18

这么一长段用源代码表示就干了稀松的事情

1
2
3
4
loc_80486B8:
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;

然后翻译成不带goto的c代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char *decrypt(char *s,char *another){
char *dest=(char *)malloc(strlen(s)+1);
strcpy(dest,s);
int var_18=0;
int var_1C=0;

while(var_1C<strlen(s)){
var_18=0;
while(var_18<strlen(another)&&var_1C<strlen(s)){
dest[var_1C]=dest[var_1C]-another[var_18];
++var_1C;
++var_18;
}
}
return dest;
}

这里两层while循环到底干了一个什么事呢?再进一步精简一下

1
2
3
4
5
6
7
8
9
10
char *decrypt(char *s,char *key){
char *dest=(char *)malloc(strlen(s)+1);
strcpy(dest,s);
int index=0;
for(int i=0;i<strlen(s);++i){
index=i%strlen(key);//计算s[i]应该减去的key数组中的哪一个元素
dest[i]-=another[index];//位移密码
}
return dest;
}

就是一个位移密码解密,s是需要解密的字符串,算法是用s 的每一个字符去减key的相应位置的字符,如果key不够长则key循环使用

回到authenticate函数

image-20220504201120562

这个函数还怪长的,不看F5伪代码,应当如何解读呢?

首先,在分析完decrypt函数之后,我们大体上可以知道,密文和密钥都是在.rodata只读区存好的,然后通过decrypt解密算法得到加密前的明文,下面要做的应该是获取键盘输入,然后将刚才的明文和键盘输入进行对比

至于为什么不直接存储明文然后和获取的键盘输入进行对比?这就好比给一个孩子一艘拼装好的乐高千年隼和一盒子千年隼零件的关系

考察孩子对加密算法的掌握,以及对加密算法汇编形式的掌握呗

然后,基于上述分析,可以推测authenticate函数要做的,大体可以分为四或者五个部分

1
2
3
4
5
6
0.开端(任何函数都有,通常是压栈保存调用者函数的帧指针rbp,申请函数栈,rbp作为当前函数帧指针)
1.解密
2.输入
3.处理输入(可以算是输入的一部分)
4.字符串对比
5.尾声(任何函数都有,通常是释放函数栈和退栈还原rbp为调用者函数的帧指针)

顺序不一定,但是也就是这几件事了,想不出来还能干什么

实际分析

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
;0.开端
.text:08048708 push ebp
.text:08048709 mov ebp, esp
.text:0804870B sub esp, 8028h

;1.解密获得字符串
.text:08048711 mov dword ptr [esp+4], offset another ; another表示密钥,其地址压栈作为第二个参数
.text:08048719 mov dword ptr [esp], offset s ; s ; s表示需要解密的字符串,地址压栈作为参数
.text:08048720 call decrypt ; 调用decrypt,他需要两个参数
.text:08048725 mov [ebp+s2], eax ; decrypt返回值->eax->s2

;2.键盘获得输入字符串
.text:08048728 mov eax, ds:stdin@@GLIBC_2_0 ;标准键盘输入
.text:0804872D mov [esp+8], eax ; stream ;stdin->eax->[esp+8]作为第三个参数
.text:08048731 mov dword ptr [esp+4], 2000h ; n ;2000h->[esp+4]作为第二个参数
.text:08048739 lea eax, [ebp+ws] ;ws->eax
.text:0804873F mov [esp], eax ; ws ;ws->eax->[esp]作为第一个参数
.text:08048742 call _fgetws ;fgetws函数共需要三个参数,都已经妥善安置入栈

;3.处理键盘输入
.text:08048747 test eax, eax ;判断是否读取到至少一个字符
;3.1如果没有获取到输入
.text:08048749 jz short loc_804879C ;啥也没读到,跳转loc_804879C尾声
;3.2如果获取到了输入
.text:0804874B lea eax, [ebp+ws] ;&ws->eax
.text:08048751 mov [esp], eax ; s ;&ws->eax->[esp]压栈作为参数
.text:08048754 call _wcslen ;&ws作为参数压栈,传递给_wcslen
.text:08048759 sub eax, 1 ;strlen(ws)-1->eax
.text:0804875C mov [ebp+eax*4+ws], 0 ;ws[strlen(ws)-1]='\0'

; 4.上述两个字符串比较
.text:08048767 mov eax, [ebp+s2] ;decrypt返回值->s2->eax
.text:0804876A mov [esp+4], eax ; s2 ;decrypt返回值压栈,作为第二个参数
.text:0804876E lea eax, [ebp+ws] ;&ws->eax
.text:08048774 mov [esp], eax ; s1 ;&ws->eax->[esp],作为第一个参数
.text:08048777 call _wcscmp ;类似strcmp,输入字符串和解密后字符串比较
.text:0804877C test eax, eax ;判断strcmp是否返回0
; 4.1如果不为0则两个字符串不相等
.text:0804877E jnz short loc_804878F ;如果不为0则跳转loc_804878F
; 4.2否哦则即0表明两个字符串相等
.text:08048780 mov eax, offset unk_8048B44 ;&"success welcome back"->eax
.text:08048785 mov [esp], eax ;"success"->[esp],作为参数
.text:08048788 call _wprintf ;打印
.text:0804878D jmp short loc_804879C ;跳转尾声
; 4.1续
.text:0804878F ; ---------------------------------------------------------------------------
.text:0804878F
.text:0804878F loc_804878F: ; CODE XREF: authenticate+76↑j
.text:0804878F mov eax, offset unk_8048BA4 ;&"Access denied"->eax
.text:08048794 mov [esp], eax ;eax->[esp]作为参数
.text:08048797 call _wprintf ;打印
.text:0804879C

;尾声
.text:0804879C loc_804879C: ; CODE XREF: authenticate+41↑j
.text:0804879C ; authenticate+85↑j
.text:0804879C mov eax, [ebp+s2]
.text:0804879F mov [esp], eax ; ptr
.text:080487A2 call _free
.text:080487A7 leave
.text:080487A8 retn
.text:080487A8 ; } // starts at 8048708
.text:080487A8 authenticate endp

分析到此算是了解了authenticate的逻辑,但是有几点是我想继续研究的

1.offset指令?

.text:08048711 mov dword ptr [esp+4], offset another ;这里offset的作用是什么?

能问出这种问题来属实是我计组学的太虚了

首先参考了博客汇编语言——转移指令(offset,jmp,jcxz) - 想54256 - 博客园 (cnblogs.com)

image-20220504204638327

又产生新问题,如果说mov si,offset s是将s的地址放到si寄存器中,那么为什么不用类似lea si,[s]的指令加载有效地址?

然后查阅了这篇博客汇编语言LEA和OFFSET区别_Baoli1008的博客-CSDN博客_lea与offset的区别

image-20220504204837123

数据定义伪指令:

数据定义伪指令

2.fgetws,fgets函数的区别?

参考fgets、fgetws | Microsoft Docs

相同点:

两个函数都有三个参数,从左向右分别为:数据存储位置,要读取的最大字符数,FILE结构体指针.

返回值都是实际读取到的字符数

不同点:

fgetwsfgets 的宽字符版本。

宽字符?

啥是宽字符?用多个字节来代表的字符称之为宽字符。

unicode是宽字符之一,但是已经实际上成为了宽字符的具体实现方法,windows上的c语言宽字符就是unicode,用两个字节表示一个字符

ASCII码表用一共字节表示一个字符,一个字节的值有2^8=256种即ASCII码最多有256个,对于键盘上出现的所有字符已经足够了

但是对于汉字,法语,日语等等各种语言,ASCII能表示的字符数真的太逊了,

而unicode码两个字节表示一个字符,两个字节的值有2^16>60000,

而相对比较复杂的汉字,大约就3000常用汉字,因此unicode是有能力表示地球上的各种字符的

image-20220504210716190

12345ABCDE一共10个字符,结尾还有一个'\0'字符,因此char cstr是11字节,wchar_t wcstr是22个字节

wchar_t类型的数组长度的函数不是strlen,是wcslen,有多少个有意义的字符(不包括结尾'\0')wcslen函数就返回多少

宽字符类型的字符串字面量前需要有L修饰,不写会报错

[错误] cannot initialize array of 'wchar_t' 从 a string literal with type array of 'char'

unicode部分码表:

类型 编码(十进制) 编码(十六进制)
小写字母(a~z) 97~122 61~7a
大写字母(A~Z) 65~90 41~5a
左右花括号{} {123,125} {7b,7d}
阿拉伯数字 48~57 30~39

3.事先存在的密文和密钥放在哪里?"Access denied"等字样又放在那里?

在authenticate函数里面双击s观察s的存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.rodata:08048AA8 ; const wchar_t s
.rodata:08048AA8 s db 3Ah ; DATA XREF: authenticate+11↑o
.rodata:08048AA9 db 14h
.rodata:08048AAA db 0 ;蜜汁0
.rodata:08048AAB db 0 ;蜜汁0
.rodata:08048AAC dd 1436h
.rodata:08048AB0 db 37h ; 7
.rodata:08048AB1 db 14h
.rodata:08048AB2 db 0 ;蜜汁0
.rodata:08048AB3 db 0
.rodata:08048AB4 db 3Bh ; ;
.rodata:08048AB5 db 14h
.rodata:08048AB6 db 0
.rodata:08048AB7 db 0
.rodata:08048AB8 db 80h
.rodata:08048AB9 db 14h
.rodata:08048ABA db 0
.rodata:08048ABB db 0
...

.rodata是常量区,在程序运行之前,在编译时就能确定

s是宽字符数组,每个元素都是wchar_t类型,占两个字节,小端存储的话就可以写为:

1
0x1434 0000 0x1436 0000 0x1437....

奇怪的是为什么其中会有很多0?为什么字符不能够紧凑存放?

能问出这种问题来属实是我c基础太差了

image-20220504235300627

sizeof('a')sizeof(char)的结果竟然不一样

查阅资料,a字符常量,却以int四字节存储

因此对rodata段的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
.rodata:08048AA8 s               db 3Ah                  ; DATA XREF: authenticate+11↑o
.rodata:08048AA9 db 14h
.rodata:08048AAA db 0
.rodata:08048AAB db 0

.rodata:08048AAC db 36h
.rodata:08048AAD db 14h
.rodata:08048AAE db 0
.rodata:08048AAF db 0

.rodata:08048AB0 db 37h ; 7
.rodata:08048AB1 db 14h
.rodata:08048AB2 db 0
.rodata:08048AB3 db 0

.rodata:08048AB4 db 3Bh ; ;
.rodata:08048AB5 db 14h
.rodata:08048AB6 db 0
.rodata:08048AB7 db 0

.rodata:08048AB8 db 80h
.rodata:08048AB9 db 14h
.rodata:08048ABA db 0
.rodata:08048ABB db 0

...

在IDA View视图上选中s的元素然后convert to array(DWORD)

image-20220504235955441
1
2
3
4
5
6
7
8
[+] Dump 0x8048AA8 - 0x8048B43 (155 bytes) :
unsigned int s[39] = {
0x0000143A, 0x00001436, 0x00001437, 0x0000143B, 0x00001480, 0x0000147A, 0x00001471, 0x00001478,
0x00001463, 0x00001466, 0x00001473, 0x00001467, 0x00001462, 0x00001465, 0x00001473, 0x00001460,
0x0000146B, 0x00001471, 0x00001478, 0x0000146A, 0x00001473, 0x00001470, 0x00001464, 0x00001478,
0x0000146E, 0x00001470, 0x00001470, 0x00001464, 0x00001470, 0x00001464, 0x0000146E, 0x0000147B,
0x00001476, 0x00001478, 0x0000146A, 0x00001473, 0x0000147B, 0x00001480, 0x00000000
};

就得到了s数组

同样的道理another字符数组也位于.rodata区

1
2
3
4
[+] Dump 0x8048A90 - 0x8048AA7 (23 bytes) :
unsigned int another[6] = {
0x00001401, 0x00001402, 0x00001403, 0x00001404, 0x00001405, 0x00000000
};

同样的道理,"Success..."等字样也位于.rodata区

image-20220505000407437

解密

用c程序解密

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
#include <stdio.h>

unsigned int s[39] = {
0x0000143A, 0x00001436, 0x00001437, 0x0000143B, 0x00001480, 0x0000147A, 0x00001471, 0x00001478,
0x00001463, 0x00001466, 0x00001473, 0x00001467, 0x00001462, 0x00001465, 0x00001473, 0x00001460,
0x0000146B, 0x00001471, 0x00001478, 0x0000146A, 0x00001473, 0x00001470, 0x00001464, 0x00001478,
0x0000146E, 0x00001470, 0x00001470, 0x00001464, 0x00001470, 0x00001464, 0x0000146E, 0x0000147B,
0x00001476, 0x00001478, 0x0000146A, 0x00001473, 0x0000147B, 0x00001480, 0x00000000
};

unsigned int another[6] = {
0x00001401, 0x00001402, 0x00001403, 0x00001404, 0x00001405, 0x00000000
};

wchar_t ans[100];

int main() {
int index = 0;
for (int i = 0; i < 38; i++) {
index = i % 5;
s[i] -= another[index];
swprintf(&ans[i], L"%s", &s[i]);
}
printf("%ls", ans);
return 0;
}

运行结果:

1
9447{you_are_an_international_mystery}

011csaw2013reversing2

信息收集

运行程序之后直接弹窗,推测是一个win32API-messageBox

image-20220506092936536

上面都是写的乱码,下面三个选择框都导致程序结束

静态分析

critical

正常运行时执行左侧逻辑,但是左侧逻辑就是用win32API-messageBox显示了一些乱码,没有卵用

因此应该审查调试运行时的逻辑,应该只要是调试运行就可以执行sub_4001000但是没有打印输出,估计调试器可以观察出某些变量的值,

但是我目前只会用gdb调试器,但是这个题给出的文件明显编译时没有-g选项,没有调试信息

还是从静态分析入手,分析解密函数sub_401000

解密函数sub_401000

image-20220506095147714

解密算法就是把s字符数组四个字符按照小端规则看成一个双字去和双字类型的key=0xDDCCAABB按位异或,然后再将双字拆成四个ascii字符即得到flag

堆上的缓冲区s拷贝的是unk_409B10,将其转化为c双字数组

image-20220506095859599
1
2
3
4
unsigned int unk_409B10[9] = {
0xBCA0CCBB, 0xB8BED1DC, 0xAEBECFCD, 0x82ABC4D2, 0xB393D9D2, 0xA993DED4, 0x82B8CBD3, 0xB9BECBD3,
0x00CCD79A
};

解密程序:

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
44
45
46
47
#include <iostream>
#include <algorithm>
using namespace std;

union IntAndChar4 {
protected:
unsigned int inInt;//inInt和inChar[4]共用四个字节
unsigned char inChar[4];
public:
void setInt(const int &i) {
inInt = i;
}
int getInt()const {
return inInt;
}
string getString()const {
string s;
for (int i = 0; i < 4; ++i) {
s += inChar[i];
}
return s;
}
friend ostream &operator<<(ostream &os, const IntAndChar4 &iac4) {
os << iac4.getString();
return os;
}
} iac4[9];

unsigned int key = 0xDDCCAABB;
unsigned int encrypted[9] = {
0xBCA0CCBB, 0xB8BED1DC, 0xAEBECFCD, 0x82ABC4D2, 0xB393D9D2, 0xA993DED4, 0x82B8CBD3, 0xB9BECBD3,
0x00CCD79A
};

void decrypt() {
for (int i = 0; i < 9; ++i) {
iac4[i].setInt(key ^ encrypted[i]);
cout << iac4[i];
}
}


int main() {
decrypt();

return 0;
}

运行结果:

1
flag{reversing_is_not_that_hard!}

012maze

尽量不看源代码,用CSAPP第三章的方法,将汇编语言首先转化为带goto和label的伪代码,然后转为不带goto和label的伪代码

这个题的名字maze很有意思,迷宫,既体现在反汇编翻译成伪代码时分支众多,又体现在最后使用走迷宫的方法获得flag

main

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
.text:00000000004006B0 ; int __fastcall main(int, char **, char **)
;注意调用方式,所有局部变量的地址都基于栈顶指针rsp计算,不基于帧指针计算
.text:00000000004006B0 main proc near ; DATA XREF: start+1D↑o
.text:00000000004006B0
.text:00000000004006B0 var_28 = dword ptr -28h
.text:00000000004006B0 var_24 = dword ptr -24h
.text:00000000004006B0
.text:00000000004006B0 ; __unwind {

;开端,参数压栈,保存寄存器
.text:00000000004006B0 push rbp
.text:00000000004006B1 push r15
.text:00000000004006B3 push r14
.text:00000000004006B5 push rbx
.text:00000000004006B6 push rax

;栈上开了两个局部变量,var_24和var_28
.text:00000000004006B7 mov [rsp+28h+var_24], 0
.text:00000000004006BF mov [rsp+28h+var_28], 0

;提醒用户输入
.text:00000000004006C6 mov edi, offset s ; "Input flag:"
.text:00000000004006CB call _puts

;获取用户输入
.text:00000000004006D0 mov edi, offset format ; "%s"
.text:00000000004006D5 mov esi, offset s1;s1作为第一个参数传递给_scanf,作为缓冲区承载输入
.text:00000000004006DA xor eax, eax
.text:00000000004006DC call _scanf

;获取输入字符串的长度并与18h=24字节进行比较
.text:00000000004006E1 mov edi, offset s1 ; s
.text:00000000004006E6 call _strlen
.text:00000000004006EB mov rbx, rax
.text:00000000004006EE cmp rbx, 18h

;如果长度不够则跳转loc_400822报告失败
.text:00000000004006F2 jnz loc_400822

;否则即输入长度为18h=24字节,验证前五个字符和最后一个字符是否是nctf{balabala}这种结构
.text:00000000004006F8 mov edi, offset s1 ; s1
.text:00000000004006FD mov esi, offset s2 ; "nctf{"
.text:0000000000400702 mov edx, 5 ; n
.text:0000000000400707 call _strncmp
.text:000000000040070C test eax, eax
.text:000000000040070E jnz loc_400822
.text:0000000000400714 movzx eax, ds:byte_6010BF[rbx]
.text:000000000040071B cmp eax, 7Dh ; '}'
.text:000000000040071E jnz loc_400822

;设定循环变量初始值
.text:0000000000400724 mov edi, offset s1 ; s
.text:0000000000400729 call _strlen
.text:000000000040072E dec rax ;strlen(s1)-1->rax指向s1的最后一个非0字符的下标
.text:0000000000400731 mov ebx, 5 ;从5开始是由于0到4这前五个字符已经判断过是nctf{了,ebx将会作为循环变量i
.text:0000000000400736 cmp rax, 5 ;判断strlen(s1)-1和5的大小,即判断s1串除了nctf{}之外有无其他字符
.text:000000000040073A jbe loc_4007EE

;其他循环体变量初始化
.text:0000000000400740 lea r14, [rsp+28h+var_28];r14=&var_28,一定要分清r14里面放的是var_28的地址
.text:0000000000400744 lea r15, [rsp+28h+var_24];r25=&var_24
.text:0000000000400749 nop dword ptr [rax+00000000h];nop指令什么也不干
.text:0000000000400750

;下面进入循环体
;始终注意:
;r14=&var_28
;r15=&var_24
;rbx中放的是循环变量i
;s1是输入的字符串
;后来会用到的asc_601060是data区的一个全局字符数组,' ******* * **** * **** * *** *# *** *** *** *********'
;rbx作为循环变量,用来遍历s1字符数组,s1[rbx]相当于s1[i]
;后面专门摘出循环体进行分析
.text:0000000000400750 loc_400750: ; CODE XREF: main+133↓j
.text:0000000000400750 movsx eax, ds:s1[rbx]
.text:0000000000400757 xor ebp, ebp
.text:0000000000400759 cmp eax, 4Eh ; 'N'
.text:000000000040075C jg short loc_400780
.text:000000000040075E movzx eax, al
.text:0000000000400761 cmp eax, 2Eh ; '.'
.text:0000000000400764 jz short loc_4007A0
.text:0000000000400766 cmp eax, 30h ; '0'
.text:0000000000400769 jnz short loc_4007BB
.text:000000000040076B mov rdi, r14
.text:000000000040076E call sub_400680;小函数,作用是修改
.text:0000000000400773 jmp short loc_4007B8
.text:0000000000400773 ; ---------------------------------------------------------------------------
.text:0000000000400775 align 20h
.text:0000000000400780
.text:0000000000400780 loc_400780: ; CODE XREF: main+AC↑j
.text:0000000000400780 movzx eax, al
.text:0000000000400783 cmp eax, 4Fh ; 'O'
.text:0000000000400786 jz short loc_4007B0
.text:0000000000400788 cmp eax, 6Fh ; 'o'
.text:000000000040078B jnz short loc_4007BB
.text:000000000040078D mov rdi, r15
.text:0000000000400790 call sub_400660
.text:0000000000400795 jmp short loc_4007B8
.text:0000000000400795 ; ---------------------------------------------------------------------------
.text:0000000000400797 align 20h
.text:00000000004007A0
.text:00000000004007A0 loc_4007A0: ; CODE XREF: main+B4↑j
.text:00000000004007A0 mov rdi, r14
.text:00000000004007A3 call sub_400670
.text:00000000004007A8 jmp short loc_4007B8
.text:00000000004007A8 ; ---------------------------------------------------------------------------
.text:00000000004007AA align 10h
.text:00000000004007B0
.text:00000000004007B0 loc_4007B0: ; CODE XREF: main+D6↑j
.text:00000000004007B0 mov rdi, r15
.text:00000000004007B3 call sub_400650
.text:00000000004007B8
.text:00000000004007B8 loc_4007B8: ; CODE XREF: main+C3↑j
.text:00000000004007B8 ; main+E5↑j ...
.text:00000000004007B8 mov bpl, al
.text:00000000004007BB
.text:00000000004007BB loc_4007BB: ; CODE XREF: main+B9↑j
.text:00000000004007BB ; main+DB↑j
.text:00000000004007BB mov esi, [rsp+28h+var_24]
.text:00000000004007BF mov edx, [rsp+28h+var_28]
.text:00000000004007C2 mov edi, offset asc_601060 ; " ******* * **** * **** * *** *# "...
.text:00000000004007C7 call sub_400690
.text:00000000004007CC test al, al
.text:00000000004007CE jz short loc_400822;此处也可能出循环,当al即sub_400690返回值为0则跳出循环
.text:00000000004007D0 inc rbx
.text:00000000004007D3 mov edi, offset s1 ; s
.text:00000000004007D8 call _strlen
.text:00000000004007DD dec rax
.text:00000000004007E0 cmp rbx, rax;判断rbx是否遍历完了s1字符串
.text:00000000004007E3 jb loc_400750


;出了循环体,下面判断bpl是否为0
.text:00000000004007E9 test bpl, bpl
.text:00000000004007EC jz short loc_40080B

;检查出循环时,var_24+8*var_28是否等于23h=35字节,如果是则报告成功
.text:00000000004007EE
.text:00000000004007EE loc_4007EE: ; CODE XREF: main+8A↑j
.text:00000000004007EE movsxd rax, [rsp+28h+var_24]
.text:00000000004007F3 movsxd rcx, [rsp+28h+var_28]
.text:00000000004007F7 movzx eax, byte ptr asc_601060[rax+rcx*8] ; " ******* * **** * **** * *** *# "...
.text:00000000004007FF cmp eax, 23h ; '#'
.text:0000000000400802 jnz short loc_40080B

;置成功
.text:0000000000400804 mov edi, offset aCongratulation ; "Congratulations!"
.text:0000000000400809 jmp short loc_400810


;置失败
.text:000000000040080B ; ---------------------------------------------------------------------------
.text:000000000040080B
.text:000000000040080B loc_40080B: ; CODE XREF: main+13C↑j
.text:000000000040080B ; main+152↑j
.text:000000000040080B mov edi, offset aWrongFlag ; "Wrong flag!"


.text:0000000000400810
.text:0000000000400810 loc_400810: ; CODE XREF: main+159↑j
.text:0000000000400810 call _puts;打印刚才存放在edi中的字符串地址指向的字符串,可能是失败或成功

;函数尾声
.text:0000000000400815 xor eax, eax
.text:0000000000400817 add rsp, 8
.text:000000000040081B pop rbx
.text:000000000040081C pop r14
.text:000000000040081E pop r15
.text:0000000000400820 pop rbp
.text:0000000000400821 retn

;置失败
.text:0000000000400822 ; ---------------------------------------------------------------------------
.text:0000000000400822
.text:0000000000400822 loc_400822: ; CODE XREF: main+42↑j
.text:0000000000400822 ; main+5E↑j ...
.text:0000000000400822 mov edi, offset aWrongFlag ; "Wrong flag!"
.text:0000000000400827 call _puts
.text:000000000040082C mov edi, 0FFFFFFFFh ; status
.text:0000000000400831 call _exit
.text:0000000000400831 ; } // starts at 4006B0
.text:0000000000400831 main endp
.text:0000000000400831
.text:0000000000400831 ; ---------------------------------------------------------------------------
.text:0000000000400836 align 20h

循环体分析

循环体是本题关键

对于循环体,摘出来翻译成伪代码

其中一些sub_400680,sub_400670,sub_400660,sub_400650都是小函数,其作用是对var_24或var_28进行加一或者减一操作,然后根据他俩的值置bpl的值是否为0

image-20220506084842338

如果想要得到congratulations的结果,必须满足:

1.循环体每执行一遍,最后的时候都要满足asc_601060[var_24+var_28*8]==' '||asc_601060[var_24+var_28*8]=='#'

2.出循环的时候asc_601060[var_24+8*var_28]='#'var_24+8*var_28=36,并且bpl不能为0

问题转化

asc_601060=" ******* * **** * **** * *** *# *** *** *** *********"

其中空格字符的下标为0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

如果要满足1的话,var_24+var_28*8=0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

要满足2的话,最终结果var_24+var_28*8=36,bpl是一个循环中附带计算的值,现在不方便讨论其取值,

但是可以确定的是,如果出循环的时候var_24或者var_28有小于0或者大于8,则bpl一定为0,

那么可以粗略的认为var_24,var_28取值都在[0,8]之间(说粗略是因为有可能var_24,var_28在循环中可以越界但是后来又退进了[0,8])

转化成一维状态转移

将上述两点分析转化成一个深度优先搜索或者说动态规划的问题

var_24,var_28的值表示为(var_24,var_28)的数对(x,y),比如(1,2)就表示var_24=1,var_28=2,

一个数对与一个整数 建立映射关系(var_24,var_28)->var_24+8*var_28

初始时状态为(0,0)->0

结束时状态为(x,y)->36

中间的合法状态映射成的整数值有0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54

状态转移有四种情况: \[ (x,y)\rightarrow\begin{cases} (x-1,y)\\ (x+1,y)\\ (x,y-1)\\ (x,y+1)\\ \end{cases} \]

下面就找一条路径,使得这个映射值从0转移到36,中途的任何数对的映射值都应当落在合法值域中

可以写一深度优先搜索实现该问题

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

vector<int> legal = {0, 1, 9, 10, 11, 13, 14, 19, 21, 26, 27, 29, 33, 34, 36, 37, 38, 42, 46, 50, 51, 52, 53, 54};

bool isLegal(const int &n) {//判断一个映射值是否输入legal合法映射值
for (auto i : legal) {
if (i == n)
return true;
}
return false;
}

bool visited[10][10];//记忆化搜索

struct Path {//记录中途经过状态的结构体
int x;
int y;
friend ostream &operator<<(ostream &os, const Path &p) {
os << "(" << p.x << "," << p.y << ")=" << p.x + p.y * 8;
return os;
}
void setCoordinate(const int &x, const int &y) {
this->x = x;
this->y = y;
}
} path[20];

char directors[20];//移动方向
bool DFS(const int &x, const int &y, int depth) {

if (x < 0 || x > 8 || y < 0 || y > 8)
return false;//粗略条件剪枝
if (visited[x][y])
return false;//记忆化搜索剪枝
visited[x][y] = true;//设置访问过
int sum = x + 8 * y;//计算映射值
if (isLegal(sum) == false)
return false;//不合法,剪枝
path[depth].setCoordinate(x, y);//合法,记录路径
if (sum == 36) {//判断映射值是否已经为终点值36
return true;
}

//向其他状态转移
directors[depth + 1] = 'o';//方向数组,记录转义方向
if (DFS(x + 1, y, depth + 1))
return true;

directors[depth + 1] = '0';
if (DFS(x, y + 1, depth + 1))
return true;

directors[depth + 1] = 'O';
if (DFS(x - 1, y, depth + 1))
return true;

directors[depth + 1] = '.';
if (DFS(x, y - 1, depth + 1))
return true;
return false;
}

int main() {
if (DFS(0, 0, 0)) {//从x=0,y=0,深度depth=0开始
//如果成功da则打印方向数组
for (int i = 1; i < 20; ++i) {
cout << directors[i];
}

}

return 0;
}

运行结果:

1
o0oo00O000oooo..OO

刚好18个字符,加上头尾的nctf{}之后

nctf{o0oo00O000oooo..OO}刚好24个字符,满足所有限制条件,因此得到了flag

转化为二维迷宫

做完了看了别人的writeup才恍然大悟

考虑为什么var_24+var_28*8这里有一个*8?

如果将asc_601060=" ******* * **** * **** * *** *# *** *** *** *********"以8字节为单位换行则得到:

1
2
3
4
5
6
7
8
  ******
* * *
*** * **
** * **
* *# *
** *** *
** *
********

星号,空格,井号的宽度不一样因此这里看上去对不齐

image-20220506091352319
image-20220506091523194

右下右右下下左下下下右右右右上上左左

翻译成oO0.四个字符就是o0oo00O000oooo..OO

同样可以得到flag