6/15简单C程序的汇编代码分析

实验环境配置为win11,VS 2022.

创建一个C程序实现一个空函数的调用

1
2
3
4
5
6
7
8
void Fucntion1(){

}

int main(){
Fucntion1();
return 0;
}

测试函数

1
2
3
4
5
6
7
8
9
void __declspec Fucntion1(){

}

int main(){
Fucntion1();
return 0;
}
该函数运行时会报错,会jmp到没有初始化的代码段。

根据所使用的编译器,编译器(根据所使用的调用约定对参数进行入栈或者使用寄存器进行传参对参数进行使用)会对
静态函数进行补充对函数的实现进行补充。使用__declspec关键字编译器将不会对自定义的静态函数进行补充,也就是说不会对这个函数进行操作,真正意义上的空函数。
__declspec关键字可以放在简单声明的开头,编译器会在不发出警报的情况下忽略位于声明中的*或者&后面以及变量标识符前面的任何__declspec关键词。
参考:https://learn.microsoft.com/zh-cn/cpp/cpp/declspec?view=msvc-170

修改上一个函数实现函数两个int类型值相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int Fucntion1(int num1,int num2){
int sum=num1+num2;

return sum;
}

int main(){
/*
num1=scanf("%d",&num1);
num2=scanf("%d",&num2);
*/
//直接使用参数
//Fuction1(num1,num2);
Fuction1(1,2);
return 0;
}

1718679046204
1718679404943

main函数对Fucntion1方法进行调用,首先会将参数传入(Function1根据调用约定从右向左将参数使用push压入栈中进行传参),然后使用call对方法进行调用,add esp,8进行堆栈平衡。

修改调用关系实现三个int类型值相加

使用Function1对两个值进行相加返回一个sum,再次调用Function1对sum和第三个值进行相加。

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 Function1(int num1,int num2){
int sum=num1+num2;

return sum;
}

int Function2(int num1,int num2,int num3){
int sum1,sum2;
sum1=Function1(num1,num2);
sum2=Function2(sum1,num3);

return sum2;
}

int main(){
/*
num1=scanf("%d",&num1);
num2=scanf("%d",&num2);
num3=scanf("%d",&num3);
*/
//直接使用带入参数
//Function2(num1,num2,num3);

Function2(1,2,3);
return 0;
}

6/16调用约定

测试代码

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 <iostream>

int __cdecl Fucntion1(int num1, int num2) {
int sum = num1 + num2;

return sum;
}

int __stdcall Fucntion2(int num1, int num2) {
int sum = num1 + num2;

return sum;
}

int __fastcall Fucntion3(int num1, int num2) {
int sum = num1 + num2;

return sum;
}

/*
int __thiscall Fucntion4(int num1, int num2) {
int sum = num1 + num2;

return sum;
}
*/

int main() {
Fucntion1(1, 2);

return 0;
}
每次动态调试时修改调用方法

__cdecl 默认调用约定 默认调用约定从右向进行
1718897552737
__cdecl调用约定使用外部堆栈平衡,在call调用Function1函数之后执行add esp,8将栈顶向下移动。

__stdcall 标准调用 默认调用约定从右向左进行
1718897876545
__stdcall调用约定使用内部堆栈平衡,ret 8对esp向下移动。
ret 8 的操作参考:https://bbs.kanxue.com/thread-139088-1.htm
__fastcall 快速调用

C++中的__thiscall调用约定 该调用约定只能使用在非静态成员函数声明中。当我们使用和C的其他约定声明的函数时会在编译阶段产生报错.

1
2
3
4
5
6
7
int __thiscall Fucntion4(int num1, int num2) {
int sum = num1 + num2;

return sum;
}

error:错误(活动) E1447 __thiscall 只能出现在非静态成员函数声明中

__thiscall 指针调用
传入两个参数时从右到左将参数使用寄存器传入堆栈进行后续的调用。但是在使用多于两个参数时,会将第1和2个参数进行ecx和edx进行传参,其他的数据使用push方法进行入栈。

参考:https://www.laruence.com/2008/04/01/116.html
参考:https://learn.microsoft.com/zh-cn/cpp/cpp/argument-passing-and-naming-conventions?view=msvc-170

6/20程序入口点

程序执行流程

执行流程如下:
1718900963347

mainCRTStartup(void)函数

1
2
3
4
5
extern "C" DWORD mainCRTStartup(LPVOID)
{
return __scrt_common_main();
}

main函数执行之前,会执行外部代码VS2022编译器会给代码加上mainCRTStartup(void)函数,return执行__scrt_common_main()函数。

__sort_common_main(void)函数

参考:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/reference/security-init-cookie?view=msvc-170&devlangs=cpp&f1url=%3FappId%3DDev16IDEF1%26l%3DZH-CN%26k%3Dk(VCRUNTIME%252F__security_init_cookie)%3Bk(__security_init_cookie)%3Bk(DevLang-C%252B%252B)%3Bk(TargetOS-Windows)%26rd%3Dtrue

1
2
3
4
5
6
7
8
9
static __forceinline int __cdecl __scrt_common_main()
{
// The /GS security cookie must be initialized before any exception handling
// targeting the current image is registered. No function using exception
// handling can be called in the current image until after this call:
__security_init_cookie();

return __scrt_common_main_seh();
}

__security_init_cookie()提供缓冲区保护,会检查堆栈上的Cookie值和全局Cookie值,存在差异会直接结束程序(类似linux的canary保护)。
__scrt_common_main函数内部执行__security_init_cookie()函数,return __scrt_common_main_seh()函数。__scrt_common_main初始化缓冲区安全检查函数。

__scrt_common_main_seh(void)函数

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
static __declspec(noinline) int __cdecl __scrt_common_main_seh()
{
if (!__scrt_initialize_crt(__scrt_module_type::exe))
//检查初始化的dll格式
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);

bool has_cctor = false;
__try
{
bool const is_nested = __scrt_acquire_startup_lock();

if (__scrt_current_native_startup_state == __scrt_native_startup_state::initializing)
{
__scrt_fastfail(FAST_FAIL_FATAL_APP_EXIT);
}
else if (__scrt_current_native_startup_state == __scrt_native_startup_state::uninitialized)
{
__scrt_current_native_startup_state = __scrt_native_startup_state::initializing;

if (_initterm_e(__xi_a, __xi_z) != 0)
return 255;

_initterm(__xc_a, __xc_z);

__scrt_current_native_startup_state = __scrt_native_startup_state::initialized;
}
else
{
has_cctor = true;
}

__scrt_release_startup_lock(is_nested);

// If this module has any dynamically initialized __declspec(thread)
// variables, then we invoke their initialization for the primary thread
// used to start the process:
_tls_callback_type const* const tls_init_callback = __scrt_get_dyn_tls_init_callback();
if (*tls_init_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_init_callback))
{
(*tls_init_callback)(nullptr, DLL_THREAD_ATTACH, nullptr);
}

// If this module has any thread-local destructors, register the
// callback function with the Unified CRT to run on exit.
_tls_callback_type const * const tls_dtor_callback = __scrt_get_dyn_tls_dtor_callback();
if (*tls_dtor_callback != nullptr && __scrt_is_nonwritable_in_current_image(tls_dtor_callback))
{
_register_thread_local_exe_atexit_callback(*tls_dtor_callback);
}

//
// Initialization is complete; invoke main...
//

int const main_result = invoke_main();

//
// main has returned; exit somehow...
//

if (!__scrt_is_managed_app())
exit(main_result);

if (!has_cctor)
_cexit();

// Finally, we terminate the CRT:
__scrt_uninitialize_crt(true, false);
return main_result;
}
__except (_seh_filter_exe(GetExceptionCode(), GetExceptionInformation()))
{
// Note: We should never reach this except clause.
int const main_result = GetExceptionCode();

if (!__scrt_is_managed_app())
_exit(main_result);

if (!has_cctor)
_c_exit();

return main_result;
}
}
7/16程序模块

程序本体作为主模块
程序模块分为两种,用户模块和系统模块。
系统模块是windows系统本体自带的模块,所有基于windows系统的程序都会使用这些模块中的一部分,是windows系统程序运行所必须的系统依赖。
用户模块是当前程序安装时自带所使用的模块,除了输入法以外。输入法会在任何当前使用的程序中导入输入法需要使用的模块。用户模块是当期程序运行所需要的用户环境依赖,只是运行这个程序所一定需要的用户依赖。

C运行时(CRT)和STL.lib文件(C++标准库)

参考:https://learn.microsoft.com/zh-cn/cpp/c-runtime-library/crt-library-features?view=msvc-170
lib分为两种,lib静态库和lib导入库。lib静态库由源代码C或是C++编译得到,包含了所有的外部代码,链接器连接后可以被正常使用(可以理解为已经编译好的代码),可以正常调用lib的外部函数。lib导入库类似于.h库的使用(头文件的作用是对函数进行声明),是库dll文件所有函数在dll文件种地址的说明。

DLL文件

参考:https://learn.microsoft.com/en-us/troubleshoot/windows-client/setup-upgrade-and-drivers/dynamic-link-library

invoke_main()

1
2
3
4
static int __cdecl invoke_main()
{
return main(__argc, __argv, _get_initial_narrow_environment());
}

main函数入口

6/20数据类型

7/15Windows API

Win API微软官方提供的统一功能实现的调用接口,包括系统信息获取,资源读取,字符串设置等一系列的功能实现。
一般程序中的Win API用于实现关键功能的,直接查Msdn可以通过API的功能实现,对程序的功能实现进行猜测或者是判定。

WinExec和ShellExec常用的命令执行API

A版本W版本API

一般存在于有字符串引用的WIN API,用于区别使用的字符串编码是ASCII或是UNICODE等其他编码。
MessageboxA和MessageboxW很典型的A版本和W版本区分的WIN API,使用了不同编码的字符串。使用API的函数不能进行确定直接断两个版本的,找实际调用的函数进行分析。

8/20类型和匈牙利表达法

Windows API表示函数原型使用了匈牙利表示法,一些参数会使用dwSize作为需要的内存空间大小,其中的dw表示了参数所使用的类型,Size表示这个参数的作用。
Alt text

句柄

句柄是一个表示符,用于标识窗口,进程,模块,菜单,文件等。类似于指针用于指向一个对象或者是某一个内存位置。而指针不能进行数学操作,而且不总是表示对象的地址。他的作用就是标识一个对象,并且保存这个句柄,在后续函数调用中使用这个句柄对指向的对象进行引用。

文件系统函数

CreateFile
ReadFile
WriteFile
CreateFileMapping
MapViewOfFile

注册表

注册表根键

HKEY_LOCAL_MACHINE(HKLM) 保存本地机器全局配置
HKEY_CURRENT_USER(HKCU) 保存当前用户特定的设置
HKEY_CLASSES_ROOT 保存定义的类型信息
HKEY_CURRENT_CONFIG 保存关于当前硬件配置的配置,特别是与当前和标准配置不同的部分
HKEY_USERS 定义默认用户,新用户和当前用户的配置

HKEY_CURRENT_USER存放HKEY_USERS\SID SID表示用户的安全描述符

.reg文件

.reg文件在管理员权限下运行后,可以对注册表进行修改(但是win11在使用.reg导入后刷新注册表显示已经导入reg配置仍然不会更改配置)
需要使用类似的功能可以使用.bat文件,写入reg工具的脚本进行相似功能的实现。

8/21 .bat批处理脚本

语法
1
2
3
@echo off 
:: echo off 为不显示这个执行结果的所使用的具体命令
:: 注释符号为两个"::"表示该行为注释

示例:

1
2
3
4
5
@echo off
:: 复制删除这一行,.bat文件不能含有中文 测试功能echo off自行注释第一行代码
reg query HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run

pause

使用@echo off
Alt text
不使用@echo off
Alt text

语法核心主要是使用命令,命令的语法根据-help去对照使用

变量

初始化变量使用set命令对变量进行声明

1
2
set /A variable-name=value
// "/A"命令作用是将初始化的变量设置为数值

注册表操作函数

RegOpenKeyEx
RegSetValueEx
RegGetValue

Windows 网络函数API

进程

windows下进程的创建

windows下使用explorer.exe 作为父进程对子进程进行启动。例如对VSCODE进行启动的情况(工具:Procmon.exe):在VSCODE启动时,开始进程行为捕获(过滤文件系统行为,注册表行为,网络行为)
Alt text
Alt text
如图进程的创建由explorer.exe进行创建,创建VSCODE进程的PID为15140.

线程

服务

组件对象模型

内核和用户模式

8/18数组

间接访问可以通过形参对实参进行修改(指针 数组下标 结构体成员指针)

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
//界面和逻辑进行分离
//业务和算法进行分类

#include<stdio.h>
#include<stdlib.h>

int swap1(int x,int y)
{
x=x+y;
y=x-y;
x=x-y;
}
void swap2(int &a, int &b)
{
a = a^b;
b = a^b;
a = a^b;
}
int main()
{
//数组的大小类型需要进行提前定义
//数组的地址是第0个成员的地址(成员的地址基地址+成员的便宜)
int ary[5]={1,2,3,4,5};
//printf("%d\n",ary[4]);
int n=2;
//寻址逻辑 地址常量+偏移*类型大小 即使这个地址计算出的值已经不在数组的定义范围内
//而且根据计算机制 甚至可以将变量和偏移进行反着写
printf("%d\n",&ary[4]);
printf("%d\n",&ary[n]);
printf("%d\n",&ary[n+3]);
printf("%d\n",&ary[n-3]);
printf("%d\n",&n[ary]);

int a=1;
int b=2;
swap2(a,b);
printf("%d,%d",a,b);
system("pause");
return 0;
}

Alt text

二维数组

二维数组的寻址公式 ary[二维数组内含一维数组的长度*二维数组第一参数+]

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
//界面和逻辑进行分离
//业务和算法进行分类

#include<stdio.h>
#include<stdlib.h>

//不使用第一种遍历二维数组对象,不适用所有的二维数组对象
void ShowArray1(int ary[][4],int nCount)
{
for(int i=0;i<nCount;i++)
{
for(int j=0;j<4;j++)
{
printf("%d\t",ary[i][j]);
}
printf("\n");
}
}

//第二种
void ShowArray2(int ary[],int nMaxX,int nMaxY)
{
for(int i=0;i<nMaxX;i++)
{
for(int j=0;j<nMaxY;j++)
{
printf("%d\t",ary[(nMaxY*i)+j]);
}
printf("\n");
}
}

int main()
{
//多维数组是特殊一维数组
//多维数组是将多个一维数组进行连续排列
int ary[3][4] = {
{1,2,3,4},
{2,2,3,3},
{3,2,3,4}
};
printf("%d\n",&ary[0][0]);
printf("%d\n",&ary);

printf("%d\n",ary[0][4*1+1]);
printf("%d\n",ary[1][1]);
//数组默认指针类型 遍历二维数组和数组必须明确数组的类型和数组寻址公式
ShowArray1(ary,3);
ShowArray2(&ary[0][0],3,4);

//地址的计算方式是类似一维数组的
//type[M][N] ary[x][y] 二维数组仍然表示这个二位数组的首地址 address=(int)ary+sizeof(type[N])*x+sizeof(type)*y=ary+sizeof(type)*(N*x+y)
//指针访问二维数组 &ary[0][N*x+y] 这个公式可以将二维数组的参数都写在第二个参数中
system("pause");
return 0;
}

8/20 字符串 字符数组

字符串是特殊的字符数组(可以理解为编码后的字符数组)
字符串可以分为两大类型风格pascal风格和C-type风格

pascal风格

先声明了字符串长度占两个字节,后面为字符串本体

C-type风格

不定义字符串长度,以”\0”作为字符串结尾

string.h库

char *strchr(const char *str, char c);
//查找字符串中第一个出现的指定字符的位置

int strcmp(const char *str1,const char *str2);
比较两个字符串的大小,区分大小写
返回值:str1 > str2 , 返回 1;str1 < str2 , 返回 -1;str1 == str2 , 返回 0;

char *strlwr(char *str);
//字符串中字符全部变为小写

char *strupr(char *str);
//字符串中所有字符全部变为大写

strcpy()函数功能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
#include<stdlib.h>

void mystrcopy(char szDst[],char szSrc[])
{
int i=0;
while(szDst[i++]=szSrc[i]);
}

int main()
{
char szBuf[20];

mystrcopy(szBuf,"Hello,World");

system("pause");
return 0;
}

二维字符串数组

定义为char szName[8][30]={“1”,”2”};
第一个定义了几个学生的ID,第二个表示学生的ID的最大长度(这是一个变长的数组)

9/5 作用域

Windows反调试

10/10互斥体创建

原理程序内部创建一个互斥体,互斥体在内核中被唯一标识,通过进程对互斥体创建的返回值进行比较。当创建互斥体失败时返回值为ERROR_ALREADY_EXIST表示这个文件的互斥体已经存在。
调试器启动程序的时候执行CreateMutex函数,会创建这个互斥体,检测到重复创建进程,则会退出当前进程。

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
#include <iostream>
#include<stdlib.h>
#include<Windows.h>
using namespace std;

BOOL IsAlreadyRun()
{
//初始化互斥体句柄
HANDLE hMutex = NULL;
hMutex = ::CreateMutex(NULL, FALSE, L"MyUniqueApplicationName");
if (hMutex)
{
if (ERROR_ALREADY_EXISTS == ::GetLastError())
{
::CloseHandle(hMutex);
return TRUE;
}
}
else
{
cout << "互斥体创建失败" << endl;
return TRUE; // 如果互斥体创建失败,也认为已经运行
}
return FALSE;
}
int main()
{
if (IsAlreadyRun())
{
cout << "检测到重复运行" << endl;
system("pause");
return 0;
}
else
{
cout << "正常运行" << endl;
system("pause");
return 0;
}
}

根据启动的父进程进行反调试

原理正常进程的父进程是Explorer.exe,但是在调试器对程序进行调试的情况下,进程的父进程是所使用的调试器(思路类似于检查StartupInfo中成员变量和正常进程中的不同)

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
// 检查父进程的反调试.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include<stdlib.h>
#include <iostream>
#include <stdio.h>
//#include <psapi.h>
#include <windows.h>
#include <tlhelp32.h>
#include <winternl.h>

using namespace std;

//定义INFORMATIONPROCESS的结构
typedef
__kernel_entry NTSTATUS
(NTAPI* NQIP)(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);

//获取当前环境中explorer.exe的PID
DWORD GetExplorerPid()
{
PROCESSENTRY32 pe32;
pe32.dwSize = sizeof(PROCESSENTRY32);

//创建当前环境的快照
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

if (hProcessSnap == INVALID_HANDLE_VALUE)
{
printf("CreateToolhelp32Snapshot_error\n");
}
//查询当前快照中的Explorer.exe
if (Process32First(hProcessSnap, &pe32))
{
do
{
//获取PID
if (wcscmp(pe32.szExeFile, L"explorer.exe") == 0)
{
return pe32.th32ProcessID;
break;
}
} while (Process32Next(hProcessSnap, &pe32));
}
else
{
printf("Process32FirstW_error:%d\n", GetLastError());
CloseHandle(hProcessSnap);
}
}

//检测调试器函数
BOOL IsDebug()
{
//获取当前环境中explorer.exe的PID
DWORD ExplorerPID = GetExplorerPid();
printf("Explorer.exe_PID:%d\n", ExplorerPID);
//获取当前程序的PID
DWORD CurrentProcessPID = GetCurrentProcessId();
//获取当前程序的句柄
HANDLE CurrrentProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, CurrentProcessPID);
//获取当前程序的父进程的PID
//获取ntdll.dll的句柄
HMODULE hNtdll = GetModuleHandle(L"ntdll.dll");
//获取NtQueryInformationProcess的函数指针
NQIP _NtQuertInformationProcess = (NQIP)GetProcAddress(hNtdll, "NtQueryInformationProcess");
/*
NtQueryInformationProcess NTAPI
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
*/
//定义pbi结构
PROCESS_BASIC_INFORMATION pbi;
/*
PROCESS_BASIC_INFORMATION结构
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
}PROCESS_BASIC_INFORMATION;
*/
//获取NTSTATUS结构参数
NTSTATUS Ntstatus = _NtQuertInformationProcess(CurrrentProcessHandle, ProcessBasicInformation, &pbi, sizeof(PROCESS_BASIC_INFORMATION), NULL);
DWORD CurrentParentProcessPID = (LONG_PTR)pbi.Reserved3;
printf("CurrentParentProcessPID:%d\n", CurrentParentProcessPID);
//比较PID返回值
if (CurrentParentProcessPID != ExplorerPID)
{
return TRUE;
}
else
{
return FALSE;
}
}

int main()
{
if (IsDebug())
{
cout << "检测到调试器" << endl;
system("pause");
return 0;
}
else
{
cout << "未检测到调试器" << endl;
system("pause");
return 0;
}
}

10/12 HOOK

全局消息钩子(SetWindowsHookEx)

全局钩子主要是使用SetWindowsHookEx API.

注入HOOK

inline HOOK

IAT HOOK

HoxFix HOOK

调试HOOK

Windows下的具体化返回API的报错原因

参考:https://learn.microsoft.com/en-us/windows/win32/Debug/retrieving-the-last-error-code

1