我的知识记录

digSelf

C++逆向:选择结构的分析、识别与还原

2021-08-31
C++逆向:选择结构的分析、识别与还原

结构化程序设计的三大结构,分别是:顺序结构、选择结构和循环结构。这三种基本结构可以任意组合,形成多姿多彩的程序世界。在这其中的顺序结构是按照代码编写的顺序逐步执行;而选择结构打断了这种顺序,让程序支持了逻辑条件。因此,可以识别与还原选择结构对于C/C++逆向工程是非常重要的。本文只进行识别和还原if-else结构,而switch-case结构已经阐述完毕,本文不再进行赘述。实验环境采用的是VS2019MSVCVC6MSVC进行编译,优化选项为速度最快优化

...

条件转移的分类

条件转移指令大致上分为三类:

  • if-else结构
  • switch-case结构
  • 三目运算

其中:switch-case的优化策略较多,因此对于它在反汇编中的识别和还原另开辟了一篇文章进行了讨论与分析。本文注重分析if-else分支结构的分析与还原以及对三目运算的简要分析,在分析时需要注意各种优化策略(如:传播、折叠、公共表达式等)优化策略对分析的影响。

各类选择结构的分析

if...else单分支结构的分析

常量做判断条件

测试代码:

int main(int argc, char** argv) {
	if (1) {
		printf("constant\n");
	}
	return 0;
}

反汇编的结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh(void)+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  4
.text:00401040 argv            = dword ptr  8
.text:00401040 envp            = dword ptr  0Ch
.text:00401040
.text:00401040                 push    offset Format   ; "constant\n"
.text:00401045                 call    sub_401010
.text:0040104A                 add     esp, 4
.text:0040104D                 xor     eax, eax
.text:0040104F                 retn
.text:0040104F _main           endp

可以发现,没有生成任何的条件转移指令。为什么呢?因为编译器在编译过程中就可以确定执行的是哪一个分支,故编译器在编译阶段就将其优化掉了,不会产生任何的条件转移指令。对于这样的代码,在底层是还原不出来上述测试代码的,但是可以还原出其等价代码。

if单分支结构

测试代码:

int main(int argc, char** argv) {
	if (argc > 2) {
		printf("argc > 2\n");
	}
	return 0;
}

反汇编的完整结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh(void)+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp             ; ArgList
.text:00401041                 mov     ebp, esp
.text:00401043                 cmp     [ebp+argc], 2
.text:00401047                 jle     short IF_END
.text:00401049                 push    offset Format   ; "argc > 2\n"
.text:0040104E                 call    sub_401010
.text:00401053                 add     esp, 4
.text:00401056
.text:00401056 IF_END:                                 ; CODE XREF: _main+7↑j
.text:00401056                 xor     eax, eax
.text:00401058                 pop     ebp
.text:00401059                 retn
.text:00401059 _main           endp

经过整理可以发现,其产生了一条条件转移指令jle short IF_END。但是好像与我们源代码中写的条件argc > 2是不同的,为什么是这样的?编译器生成的汇编代码还是正确的吗?答案是生成的汇编代码是正确的。原因是对于高级代码(如C语言的代码)而言,如果条件成立则执行条件体内部的代码;而对于汇编指令来说,如果条件成立则跳转成立,就会将执行流程转移了。所以在生成汇编代码的时候,需要对原条件进行取反,才能保证其逻辑上与高级代码的逻辑是一致的,即:条件成立,执行条件体内的代码。

根据上述特点,可以总结单分支转移结构的特点:

  • 有一条jcc指令
  • jcc指令后面会紧跟一个标号
  • 标号上面没有其他的跳转
  • 这个跳转跳过一段代码

根据这些特点就可以将单分支结构给识别出来了,还原时,只需要将jcc指令和标号中间的代码还原到条件体内部,而判断条件为jcc指令条件取反即可还原。

if...else结构

测试代码:

int main(int argc, char** argv) {
	if (argc > 2) {
		printf("argc > 2\n");
	}
	else {
		printf("default\n");
	}
	return 0;
}

VS2019的反汇编结果

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 cmp     [ebp+argc], 2
.text:00401047                 mov     ecx, offset _Format ; "argc > 2\n"
.text:0040104C                 mov     eax, offset aDefault ; "default\n"
.text:00401051                 cmovg   eax, ecx
.text:00401054                 push    eax             ; _Format
.text:00401055                 call    _printf
.text:0040105A                 add     esp, 4
.text:0040105D                 xor     eax, eax
.text:0040105F                 pop     ebp
.text:00401060                 retn
.text:00401060 _main           endp

可以发现没有使用jcc指令,而是使用了新的指令cmovg ,它们是一系列的条件赋值指令,指令功能为:当条件成立时,则执行赋值操作。对于本例来说,其优化后的反汇编代码还原后为:

int __cdecl main(int argc, const char **argv, const char **envp) {
    const char *str = "default\n";
    if (argc > 2) {
        str = "argc > 2\n";
    }
    printf(str);
    return 0;
}

即:编译器首先假设是default的情况,然后再进行判断其是否大于2,编译器这么做是与源代码等价的。

MSVC的高版本编译器使用了条件赋值指令来实现了无分支的操作,那么在不支持条件赋值指令的CPU上是如何操作的呢?现在用VC6MSVC进行编译测试一下(因为VC6的那个年代,没有这条指令,所以可以观察VC6下的编译结果),观察其反汇编结果:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 cmp     [esp+arg_0], 2
.text:00401005                 jle     short ELSE_BEGIN
.text:00401005
.text:00401007                 push    offset s->Argc>2 ; "argc > 2\n"
.text:0040100C                 call    printf
.text:0040100C
.text:00401011                 add     esp, 4
.text:00401014                 xor     eax, eax
.text:00401016                 retn
.text:00401016
.text:00401017 ; ---------------------------------------------------------------------------
.text:00401017
.text:00401017 ELSE_BEGIN:                             ; CODE XREF: _main+5j
.text:00401017                 push    offset s->Default ; "default\n"
.text:0040101C                 call    printf
.text:0040101C
.text:00401021                 add     esp, 4
.text:00401024
.text:00401024 ELSE_END:
.text:00401024                 xor     eax, eax
.text:00401026                 retn
.text:00401026
.text:00401026 _main           endp

可以发现,其还是使用的条件转移指令进行实现的。由于采用的是速度最快的优化,其优化方式如图所示:

所以没有使用无条件跳转转移指令来连接公共的代码块,而是利用公共表达式优化,将公共代码块复制一份,抵消了一次跳转,从而没有打断CPU的流水线优化,提高了运行速度。

if...else的代码特征:

  • 有多个jcc指令,每个jcc指令后都有一个标号
  • 标号之间为各个条件块内的代码

而对于还原方法,其还原的方法与if单分支结构的还原方法一致。

if...else if多分支结构的分析

if...else if结构

测试代码:

int main(int argc, char** argv) {
	if (argc > 2) {
		printf("argc > 2\n");
	}
	else if (argc < 4) {
		printf("argc < 4\n");
	}
	return 0;
}

VS2019版本的完整的反汇编结果如下:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 cmp     [ebp+argc], 2
.text:00401047                 mov     ecx, offset _Format ; "argc > 2\n"
.text:0040104C                 mov     eax, offset aArgc4 ; "argc < 4\n"
.text:00401051                 cmovg   eax, ecx
.text:00401054                 push    eax             ; _Format
.text:00401055                 call    _printf
.text:0040105A                 add     esp, 4
.text:0040105D                 xor     eax, eax
.text:0040105F                 pop     ebp
.text:00401060                 retn
.text:00401060 _main           endp

可以发现,其也是使用的comvg来实现的无分支转移。对于低版本的编译器,如VC6MSVC的情况,反汇编结果如下:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 mov     eax, [esp+arg_0]
.text:00401004                 cmp     eax, 2
.text:00401007                 jle     short ELSE_IF
.text:00401007
.text:00401009                 push    offset s->Argc>2 ; "argc > 2\n"
.text:0040100E                 call    printf
.text:0040100E
.text:00401013                 add     esp, 4
.text:00401016                 xor     eax, eax
.text:00401018                 retn
.text:00401018
.text:00401019 ; ---------------------------------------------------------------------------
.text:00401019
.text:00401019 ELSE_IF:                                ; CODE XREF: _main+7j
.text:00401019                 cmp     eax, 4
.text:0040101C                 jge     short EXIT_PROGRAM
.text:0040101C
.text:0040101E                 push    offset s->Argc<4 ; "argc < 4\n"
.text:00401023                 call    printf
.text:00401023
.text:00401028                 add     esp, 4
.text:00401028
.text:0040102B
.text:0040102B EXIT_PROGRAM:                           ; CODE XREF: _main+1Cj
.text:0040102B                 xor     eax, eax
.text:0040102D                 retn
.text:0040102D
.text:0040102D _main           endp

使用的还是公共代码块提取出来,以减少一次跳转,提高运行速度。

if...else if...else结构

测试代码:

int main(int argc, char** argv) {
	if (argc > 2) {
		printf("argc > 2\n");
	}
	else if (argc < 0) {
		printf("argc < 0\n");
	}
	else {
		printf("argc >= 0 and argc <= 2\n");
	}
	return 0;
}

VS2019MSVC的反汇编结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 mov     ecx, [ebp+argc]
.text:00401046                 cmp     ecx, 2
.text:00401049                 jle     short ELSE_IF
.text:0040104B                 mov     eax, offset _Format ; "argc > 2\n"
.text:00401050                 push    eax             ; _Format
.text:00401051                 call    _printf
.text:00401056                 add     esp, 4
.text:00401059                 xor     eax, eax
.text:0040105B                 pop     ebp
.text:0040105C                 retn
.text:0040105D ; ---------------------------------------------------------------------------
.text:0040105D
.text:0040105D ELSE_IF:                                ; CODE XREF: _main+9↑j
.text:0040105D                 test    ecx, ecx
.text:0040105F                 mov     edx, offset aArgc0AndArgc2 ; "argc >= 0 and argc <= 2\n"
.text:00401064                 mov     eax, offset aArgc0 ; "argc < 0\n"
.text:00401069                 cmovns  eax, edx
.text:0040106C                 push    eax             ; _Format
.text:0040106D                 call    _printf
.text:00401072                 add     esp, 4
.text:00401075                 xor     eax, eax
.text:00401077                 pop     ebp
.text:00401078                 retn
.text:00401078 _main           endp
.text:00401078

其首先判断是否是大于2的,如果不是再使用条件赋值语句来减少一次跳转。对于其高版本的还原是比较好做等价代码的还原的。对于低版本的MSVC的反汇编结果如下:

.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main           proc near               ; CODE XREF: start+AFp
.text:00401000
.text:00401000 arg_0           = dword ptr  4
.text:00401000
.text:00401000                 mov     eax, [esp+arg_0]
.text:00401004                 cmp     eax, 2
.text:00401007                 jle     short ELSE_IF
.text:00401007
.text:00401009                 push    offset s->Argc>2 ; "argc > 2\n"
.text:0040100E                 call    printf
.text:0040100E
.text:00401013                 add     esp, 4
.text:00401016                 xor     eax, eax
.text:00401018                 retn
.text:00401018
.text:00401019 ; ---------------------------------------------------------------------------
.text:00401019
.text:00401019 ELSE_IF:                                ; CODE XREF: _main+7j
.text:00401019                 test    eax, eax
.text:0040101B                 jge     short ELSE
.text:0040101B
.text:0040101D                 push    offset s->Argc<0 ; "argc < 0\n"
.text:00401022                 call    printf
.text:00401022
.text:00401027                 add     esp, 4
.text:0040102A                 xor     eax, eax
.text:0040102C                 retn
.text:0040102C
.text:0040102D ; ---------------------------------------------------------------------------
.text:0040102D
.text:0040102D ELSE:                                   ; CODE XREF: _main+1Bj
.text:0040102D                 push    offset s->Argc>0AndArgc<2 ; "argc >= 0 and argc <= 2\n"
.text:00401032                 call    printf
.text:00401032
.text:00401037                 add     esp, 4
.text:0040103A                 xor     eax, eax
.text:0040103C                 retn
.text:0040103C
.text:0040103C _main           endp

使用的是公共代码优化和jcc指令来实现的多分支结果,其还原方法还是对jcc的条件取反再进行还原即可。

三目运算的分析

测试代码:

int main(int argc, char** argv) {
	printf("%d\n", argc > 2 ? argc - 1 : argc + 10);
	return 0;
}

VS2019MSVC的反汇编结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 or      ecx, 0FFFFFFFFh
.text:00401046                 mov     edx, 0Ah
.text:0040104B                 cmp     [ebp+argc], 2
.text:0040104F                 cmovle  ecx, edx
.text:00401052                 add     ecx, [ebp+argc]
.text:00401055                 push    ecx
.text:00401056                 push    offset _Format  ; "%d\n"
.text:0040105B                 call    _printf
.text:00401060                 add     esp, 8
.text:00401063                 xor     eax, eax
.text:00401065                 pop     ebp
.text:00401066                 retn
.text:00401066 _main           endp

使用的还是条件赋值指令,提高运行速度。对于其还原只需要关注条件赋值指令,它本质上的逻辑还是先假设一个条件预先成立,然后再判断其余剩下的条件,根据条件成立与否来进行值的替换。

补充:对于复杂条件的分支结构的识别与还原

在写代码的过程中,最常见的就是经过逻辑与和逻辑或来进行条件组合。那么对于这些复杂的条件如何识别和还原呢?先对与条件、或条件的单独情况进行观察分析,然后再对复杂的组合条件来进行分析。

与条件

测试代码:

int main(int argc, char** argv) {
	if (argv != nullptr && argc > 2) {
		printf("argv != nullptr && argc > 2\n");
	}
	return 0;
}

VS2019MSVC下的反汇编结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 cmp     [ebp+argv], 0
.text:00401047                 jz      short EXIT_PROGRAM
.text:00401049                 cmp     [ebp+argc], 2
.text:0040104D                 jle     short EXIT_PROGRAM
.text:0040104F                 push    offset _Format  ; "argv != nullptr && argc > 2\n"
.text:00401054                 call    _printf
.text:00401059                 add     esp, 4
.text:0040105C
.text:0040105C EXIT_PROGRAM:                           ; CODE XREF: _main+7↑j
.text:0040105C                                         ; _main+D↑j
.text:0040105C                 xor     eax, eax
.text:0040105E                 pop     ebp
.text:0040105F                 retn
.text:0040105F _main           endp

可以观察到,对于多个jcc指令后面的标号都有一个统一的标号。这个特点符合逻辑运算的性质,即:对于用与运算连接的逻辑表达式而言,如果一个条件为假,则整个表达式为假。因此jcc后面的标号是退出条件体的标号,而退出标号和最后一条jcc指令之间的代码则是条件体内的代码。

还原方法则是:

  • jcc的各个条件取反
  • 将各个条件使用&&进行组合
  • 还原条件体内的代码即可

或条件

测试代码:

int main(int argc, char** argv) {
	if (argv != nullptr || argc > 2) {
		printf("argv != nullptr || argc > 2\n");
	}
	return 0;
}

VS2019MSVC下的反汇编结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043
.text:00401043 IF_BEGIN:
.text:00401043                 cmp     [ebp+argv], 0
.text:00401047                 jnz     short IF_BODY
.text:00401049                 cmp     [ebp+argc], 2
.text:0040104D                 jle     short EXIT_PROGRAM
.text:0040104F
.text:0040104F IF_BODY:                                ; CODE XREF: _main+7↑j
.text:0040104F                 push    offset _Format  ; "argv != nullptr || argc > 2\n"
.text:00401054                 call    _printf
.text:00401059                 add     esp, 4
.text:0040105C
.text:0040105C EXIT_PROGRAM:                           ; CODE XREF: _main+D↑j
.text:0040105C                 xor     eax, eax
.text:0040105E                 pop     ebp
.text:0040105F                 retn
.text:0040105F _main           endp

可以发现,其代码特征是:

  • 除了最后一个jcc外,后面的标号都是指向同一个条件体内的代码块的起始位置
  • 最后一个jcc后的标号为退出条件体的代码块的起始位置

上述代码特征符合或运算的逻辑表达式性质:对于用或连接的逻辑表达式而言,如果一个条件成立,整个表达式为真。

还原时,只需要:

  • 除了最后一个jcc的条件需要取反以外,其余的jcc条件都不取反
  • 使用或运算连接各个条件
  • 还原条件体内的代码

与运算和或运算的组合

对于组合的情况,其会将各种优化手段用上,我们需要做的就是去掉这些优化,来还原代码。

测试代码:

int main(int argc, char **argv) {
	if (argv != nullptr && argc != 1) {
		printf("%d\n", argc + 1);
	}
	else if (argc == 2 || argc == *(int*)(*argv)) {
		printf("argv is not nullptr and argc == argv");
	} 
	else {
		printf("%p\n", argv);
	}

	return 0;
}

VS2019MSVC下的反汇编结果:

.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main           proc near               ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc            = dword ptr  8
.text:00401040 argv            = dword ptr  0Ch
.text:00401040 envp            = dword ptr  10h
.text:00401040
.text:00401040                 push    ebp
.text:00401041                 mov     ebp, esp
.text:00401043                 mov     edx, [ebp+argv]
.text:00401046                 mov     ecx, [ebp+argc]
.text:00401049                 test    edx, edx
.text:0040104B                 jz      short loc_401067
.text:0040104D                 cmp     ecx, 1
.text:00401050                 jz      short loc_40106C
.text:00401052                 lea     eax, [ecx+1]
.text:00401055                 push    eax
.text:00401056                 push    offset _Format  ; "%d\n"
.text:0040105B                 call    _printf
.text:00401060                 add     esp, 8
.text:00401063                 xor     eax, eax
.text:00401065                 pop     ebp
.text:00401066                 retn
.text:00401067 ; ---------------------------------------------------------------------------
.text:00401067
.text:00401067 loc_401067:                             ; CODE XREF: _main+B↑j
.text:00401067                 cmp     ecx, 2
.text:0040106A                 jz      short loc_401084
.text:0040106C
.text:0040106C loc_40106C:                             ; CODE XREF: _main+10↑j
.text:0040106C                 mov     eax, [edx]
.text:0040106E                 cmp     ecx, [eax]
.text:00401070                 jz      short loc_401084
.text:00401072                 push    edx
.text:00401073                 push    offset aP       ; "%p\n"
.text:00401078                 call    _printf
.text:0040107D                 add     esp, 8
.text:00401080                 xor     eax, eax
.text:00401082                 pop     ebp
.text:00401083                 retn
.text:00401084 ; ---------------------------------------------------------------------------
.text:00401084
.text:00401084 loc_401084:                             ; CODE XREF: _main+2A↑j
.text:00401084                                         ; _main+30↑j
.text:00401084                 push    offset aArgvIsNotNullp ; "argv is not nullptr and argc == argv"
.text:00401089                 call    _printf
.text:0040108E                 add     esp, 4
.text:00401091                 xor     eax, eax
.text:00401093                 pop     ebp
.text:00401094                 retn
.text:00401094 _main           endp

可以观察到上面的反汇编结果好像与上面分析的没有几个是一样的,怎么办呢?一点点的看。首先观察第一个部分:

.text:00401043                 mov     edx, [ebp+argv]
.text:00401046                 mov     ecx, [ebp+argc]
.text:00401049                 test    edx, edx
.text:0040104B                 jz      short loc_401067
.text:0040104D                 cmp     ecx, 1
.text:00401050                 jz      short loc_40106C
.text:00401052                 lea     eax, [ecx+1]
.text:00401055                 push    eax
.text:00401056                 push    offset _Format  ; "%d\n"
.text:0040105B                 call    _printf
.text:00401060                 add     esp, 8
.text:00401063                 xor     eax, eax
.text:00401065                 pop     ebp
.text:00401066                 retn

可以发现其既不像与运算连接的条件(没有统一的退出标号)也不像或运算连接的条件(没有统一的入口标号),所以需要模拟执行。模拟执行后可以发现test edx, edxcmp ecx, 1是与条件,因为只有他们两个都不成立的情况下,才能执行条件体内的代码。继续向下查看:

.text:00401067 loc_401067:     ;  test    edx, edx    jz      short loc_401067 edx == argv
.text:00401067                 cmp     ecx, 2
.text:0040106A                 jz      short ELSE_BEGIN
.text:0040106C
.text:0040106C loc_40106C:     ;  cmp     ecx, 1      jz      short loc_40106C ecx == argc
.text:0040106C                 mov     eax, [edx]
.text:0040106E                 cmp     ecx, [eax]
.text:00401070                 jz      short ELSE_BEGIN
.text:00401072                 push    edx
.text:00401073                 push    offset aP       ; "%p\n"
.text:00401078                 call    _printf
.text:0040107D                 add     esp, 8
.text:00401080                 xor     eax, eax
.text:00401082                 pop     ebp
.text:00401083                 retn
.text:00401084 ; ---------------------------------------------------------------------------
.text:00401084
.text:00401084 ELSE_BEGIN:                             ; CODE XREF: _main+2A↑j
.text:00401084                                         ; _main+30↑j
.text:00401084                 push    offset aArgvIsNotNullp ; "argv is not nullptr and argc == argv"
.text:00401089                 call    _printf
.text:0040108E                 add     esp, 4
.text:00401091                 xor     eax, eax
.text:00401093                 pop     ebp
.text:00401094                 retn

这块代码块好像与条件的特征,但是它真的是吗?如果是与条件相连接的话,它们就应该是一个整体,条件判断的入口应该都是标号loc_401067的位置。虽然它们都有统一的退出出口,但是进入这个条件的入口却不是相同的,因此这两个条件判断不是与运算连接的。那么它们是什么呢?需要模拟执行一下。当两个条件各自成立时,发现有共同的入口,符合用或条件连接的代码特征,且与上面的判断条件有多个入口逻辑上可以解释的通顺,则可以判定其为或条件连接的代码特征。

综上,可以还原出代码:

int __cdecl main(int argc, const char **argv, const char **envp) {
    if (argv != nullptr && argc != 1) {
        printf("%d\n", argc + 1);
        return 0;
    }
    
    if (argc == 2 || argc == *(int*)argv) {
        printf("argv is not nullptr and argc == argv");
        return 0;
    }
    
    printf("%p\n", argv);
    return 0;
}

分析还原出来的代码和源代码进行比较,发现其在逻辑上是等价的,故还原的没有问题。

  • 0