博客园:C++常见面试题
绿盟科技研究院致力于跟踪国内外最新网络安全攻防技术,天机、天枢、星云、格物、伏影五大实验室分别专注于攻防对抗技术,数据智能,云计算安全,工业互联网、物联网、车联网和威胁追踪研究,在基础安全研究和前沿安全领域进行积极的探索,为绿盟科技的核心竞争力和持续创新能力提供了有力的保障。抗拒绝服务攻击系统(ADS)、安全和漏洞管理(AIRO)、网络入侵防护系统(IDPS)、Web应用防火墙(WAF)等多款产品,知名度很高
都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new 会自动调用对象的构造函数。delete 会调用对象的destructor,而free 不会调用对象的destructor.
malloc和new从申请的内存所在位置、返回类型安全性、内存分配失败时的返回值、是否需要指定内存大小这四点区分。
申请的内存所在位置不同。new操作符从自由存储区(free store)上为对象动态分配内存空间。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。malloc函数从堆上动态分配内存。堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。
返回类型安全性不同。new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
内存分配失败时的返回值不同。new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL。malloc分配内存失败时返回NULL。
是否需要指定内存大小不同。使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。malloc则需要显式地指出所需内存的尺寸。
若有异常则通过throw操作创建一个异常对象并抛掷。
将可能抛出异常的程序段嵌在try块之中。控制通过正常的顺序执行到达try语句,然后执行try块内的保护段。
如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从try块后跟随的最后一个catch子句后面的语句继续执行下去。
如果发生异常, catch子句按其在try块后出现的顺序被检查。匹配的catch子句将捕获并处理异常(或继续抛掷异常)。
如果匹配的处理器未找到,则运行函数terminate将被自动调用,其缺省功能是调用abort终止程序。
不准备处理的异常,可以在catch的最后一个分支,使用throw语法,向上扔。
函数重载:
函数重载的作用:
重载函数通常用来在同一个作用域内用同一个函数名命名一组功能相似的函数,这样做减少了函数名的数量,避免了名字空间的污染,提高了程序的可读性。
运算符重载:
运算符重载其实就是定义一个函数,在函数体内实现想要的功能,当用到该运算符时,编译器会自动调用这个函数。也就是说,运算符重载是通过函数实现的,它本质上是函数重载。
运算符重载的格式为:
返回值类型 operator 运算符名称 (形参表列){
//TODO:
}
总结
1. 互斥量和临界区的作用很相似,但互斥量是能够命名的,也就是说他能够跨越进程使用。所以创建互斥量需要的资源更多,所以假如只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。因为互斥量是跨进程的互斥量一旦被创建,就能够通过名字打开他。
2. 互斥量(Mutex),信号灯(Semaphore),事件(Event)都能够被跨越进程使用来进行同步数据操作,而其他的对象和数据同步操作无关,但对于进程和线程来讲,假如进程和线程在运行状态则为无信号状态,在退出后为有信号状态。所以能够使用WaitForSingleObject来等待进程和线程退出。
3. 通过互斥量能够指定资源被独占的方式使用,但假如有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,能够根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候假如利用互斥量就没有办法完成这个需要,信号灯对象能够说是一种资源计数器
PostMessage 和SendMessage的区别主要在于是否等待其他程序消息处理完成。
PostMessage只是把消息放入队列,不管其他程序是否处理都返回,然后继续执行。
而SendMessage则必须等待其他程序处理消息完成后才返回继续执行。由于SendMessage消息不放进消息队列, 所以PreTranslateMessage里无法收到其消息。
这两个函数的返回值也不同
原型:
BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam);
LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM IParam);
SendMessage的返回值表示其他程序消息处理函数的返回值。
PostMessage的返回值仅表示PostMessage函数执行是否成功,成功返回非零,否则返回零。
jmp无条件跳转,无返回,没有压栈(起到保护数据的作用)。call通过入口地址跳转有返回,返回地址压入堆栈。
堆栈空间分配区别
栈(操作系统):由操作系统(编译器)自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式类似于链表。
堆栈缓存方式区别
栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放。
堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
堆栈数据结构区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序。
栈(数据结构):一种先进后出的数据结构。
系统处理申请的区别
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的 首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
INC op:
inc指令对操作数op加1(增量),它是一个单操作数指令。操作数可以是寄存器或存储器。由于增量指令主要用于对计数器和地址指针的调整,所以它不影响进位标志CF,对其他状态标志位的影响与add、ado指令一样。
ADD op1 op2:
op1与op2相加,存在op1的位置。是双操作数指令,操作数的长度必须相同。ADD可能会改变进位标志CF。
大端:高字节保存在低地址
小端:低字节保存在低地址
.h头文件中的ifndef/define/endif 的作用是防止头文件被重复引用。
“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复引用。
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
面向对象的三个基本特征
封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)
继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。
多态:系统能够在运行时,能够根据其类型确定调用哪个重载的成员函数的能力,称为多态性。
从定义上来说:
重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数:function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关!
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
描述内存分配方式以及它们的区别?
1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。
参考
参考1
参考2
在最后一个节表位置添加一个IMAGE_SECTION_HEADER。如果没有空白位置.自己需要给扩展头扩大,修改SizeOfHeaders,并且修正所有节的偏移
修改文件头中节表个数NumberOfSections
添加的新节表修改节的读写属性.、节装载到内存的RVA 、节数据的大小、节的文件偏移
修改扩展头的PE镜像大小. sizeofImage,增加一个或数个内存页的大小。
白加黑远控木马分析
IDA、OllyDbg、x64dbg、C32ASM
加壳:其实是利用特殊的算法,对EXE、DLL文件里的资源进行压缩,
改变其原来的特征码,隐藏一些字符串等等,使一些资源编辑软件不能
正常打开或者修改。
花指令:花指令是程序中的无用代码,程序对它没影响,少了它也能正常运行。加花指令后,杀毒软件对木马静态反汇编时,木马的代码就不会正常显示出来,加大杀毒软件的查杀难度。
知了堂信安笔记 window入侵排查
Windows主机入侵痕迹排查办法
必要条件:
实现:
其他注意点:
避免二次感染:重复感染目标程序会导致其无法执行。
劫持DLL就是要制作一个“假”的DLL,但是功能又不能失真。
可执行文件在调用某函数时,要加载该函数所在的DLL。如果我们伪造一个DLL,让它包含所有被劫持DLL的导出函数。根据加载顺序,可执行文件会运行加载伪造的DLL,在伪造DLL里面做我们自己想做的事情。
防御策略:
保护游戏目录,不是自己的程序不让拷贝。(主要是防止被加入恶意的DLL到游戏的目录,驱动实现)。
创建一份游戏模块的白名单,游戏启动时对游戏目录下的文件进行检查,检查可疑的文件。白名单可本地加密存储。
将容易被劫持的Windows DLL 写进注册表,那么凡是此项下的DLL文件就会被禁止从EXE自身目录下调用,而只能从系统目录,也就是system32目录下调用。防止从游戏目录加载其它DLL。
确保windows文件保护功能是打开的。“计算机配置”→“管理模板”→“系统”→“Windows 文件保护”.
将游戏的DLL打上签名,防止自身的DLL被劫持。游戏运行时检查游戏目录下模块的签名。(没有签名拒绝加载–必须确保检查签名服务打开)
注意:通过命令行“net stop cryptsvc”关闭签名检验服务可使得客户端签名校验失效。
使用Process Monitor 检查游戏进程的可劫持的DLL 。
ring0是指CPU的运行级别,ring0是最高级别,ring1次之,ring2更次之…… 拿Linux+x86来说, 操作系统(内核)的代码运行在最高运行级别ring0上,可以使用特权指令,控制中断、修改页表、访问设备等等。 应用程序的代码运行在最低运行级别上ring3上,不能做受控操作。如果要做,比如要访问磁盘,写文件,那就要通过执行系统调用(函数),执行系统调用的时候,CPU的运行级别会发生从ring3到ring0的切换,并跳转到系统调用对应的内核代码位置执行,这样内核就为你完成了设备访问,完成之后再从ring0返回ring3。这个过程也称作用户态和内核态的切换。
在Windows日志里发现入侵痕迹
首先要判断此RVA在哪一节。
依次计算各节的RVA范围:起始RVA 到 起始RVA+节数据大小
判断出在哪一节之后,用此RVA减去此节的起始RVA,得到一个偏移量。
无论是在内存中还是在文件中,这个偏移量都是相同的。
而节的FileOffset在节表中存着(IMAGE_SECTION_HEADER->PointerTORawData)
所以可以得到此RVA对应的FileOffset = 节FileOffset + 目标RVA相对于节的偏移量。
26种对付反调试的方法
首先使用OpenProcess打开指定PID的进程,权限要足够,不然可能出现无法创建线程的情况。
第二,获得dll文件的绝对路径的字符串长度,并使用VirtualAllocEx在被注入进程中申请相应大小的内存。
第三,使用WriteProcessMemory将dll文件的绝对路径字符串写入被注入进程。
第四,以dll文件的句柄、要查找的函数名作为GetProcAddress的参数,查找本进程中加载的Kernel32.dll中LoadLibrary函数的始址,根据是否采用UNICODE有不同的函数W/ A。因为这个dll在所有进程中加载的位置都相同,所以本进程中的函数位置就是被注入进程中函数的位置。
第五,在被注入进程中使用CreateRemoteThread创建线程,参数为线程函数(LoadLibrary)地址,线程函数参数(dll文件的绝对路径)
第六,若成功注入,因为dll是首次被load,所以调用其DllMain时,fdwReason参数的值为DLL_PROCESS_ATTACH,判断一下,会弹窗提示成功。
代码远程线程注入,实质是自定义线程函数,然后将线程函数代码和线程函数中用到的数据分别注入到目标进程,最后将数据作为线程函数的参数创建远程线程,执行自定义操作。
示例:不依赖自定义dll,在被注入进程中创建线程实现弹窗,窗口标题为被注入进程的位置。
线程函数:
线程函数要做的事情是弹窗,弹窗需要得到MessageBoxA在被注入进程中的位置,因此需要加载user32.dll,还需要字符串"MessageBoxA"。加载要用到Kernel32.dll中的LoadLibrary、GetProcAddress,还需要字符串"user32.dll",在标题显示被注入进程的位置需要Kernal32.dll中的GetModuleFileName获取进程当前的运行目录。
上述用到的数据,在注入函数创建线程函数时被封装为一个结构体作为参数传给线程函数。
线程函数获得数据,调用被注入进程中的MessageBoxA弹窗
注入函数:
获取线程函数要用到的Kernel32.dll中的导出函数地址,用函数指针存放其在本进程中的地址,Kernel32.dll在各进程中位置相同,所以可用其在本进程的地址代替在被注入进程中的地址
上述所有线程函数用到的函数地址、数据等,再加一个弹窗要显示的字符串,封装到一个结构体DATA中。
将结构体、线程函数的代码注入进程
创建远程线程,将结构体作为参数传给线程函数。
加壳基础
BOOL PELoader::JudgePEFile() {
// 拿到DOS头,始址为:内存映像始址
this->pImageDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;
// 判断MZ
if (this->pImageDosHeader->e_magic != IMAGE_DOS_SIGNATURE){
std::cout << "MZ judge failed." << std::endl;
return false;
}
// 拿到PE头,始址为:内存映像始址 + DOS头中存的PE头起始位置
this->pImageNtHeaders = (PIMAGE_NT_HEADERS32)((DWORD64)pFileBuffer + this->pImageDosHeader->e_lfanew);
// 判断是否为PE文件
if (pImageNtHeaders->Signature != IMAGE_NT_SIGNATURE){
std::cout << "PE judge failed." << std::endl;
return false;
}
}
PE:模拟Windows加载器
Windows:32位Inline Hook
病毒特征码查杀之基本原理
病毒常用API
1)在某些情况下屏幕上会出现一些异常字符或某些图片;
2)文件长度异常增加或减少,或者莫名其妙地生成了新文件;
3)某些文件异常打开或突然丢失;
4)系统无缘无故地执行大量磁盘读写操作,或者未经用户许可执行格式化操作;
5)系统异常重启,经常崩溃或蓝屏无法进入系统;
6)可用内存或硬盘空间变小;
7)打印机等外部设备工作异常;
8)在正常的汉字库中,汉字无法调用和打印,或者汉字库无故损坏;
9)该扇区无故损坏;
10)程序或数据神秘地消失了,无法识别文件名,等等。