admin管理员组

文章数量:1530983

c++/Windows逆向面试题

    • **自己随便整理的,知识点并不全面。!!!**
    • 什么是线程?线程和进程的区别是什么?
    • 如何创建一个线程?
    • 什么是线程安全?
    • 死锁是怎么产生的,如何避免死锁?
    • 互斥锁、自旋锁和读写锁的区别是什么?
    • 线程间同步方式
    • 进程间通信的6种方式
    • 中断和异常的区别
    • 堆内存和栈内存有什么区别?
    • TCP和UDP的区别是什么?
    • 远程线程注入
    • 钩子hook
    • 调试器:
    • 调试框架
    • 软件断点:
    • 内存断点:
    • 硬件断点:
    • 反调试:
    • LoadPE
    • 节表注入
    • 导出表
    • 导入表
    • 重定位表
    • dump
    • dump对抗:
    • TLS 表
    • TLS回调函数

自己随便整理的,知识点并不全面。!!!

什么是线程?线程和进程的区别是什么?

线程是指计算机并发执行程序时的最小单位。一个进程可以拥有多个线程,而进程则是一个运行中的应用程序实例。线程可以共享进程中的数据和资源,是一种轻量级的并发方式。与进程相比,线程的创建、撤销和切换都比较快速,同时也占用较少的系统资源。

进程和线程的主要区别在于:前者拥有独立的地址空间和系统资源,而后者共享这些资源。在一个进程中创建多个线程可以使得各个线程之间可以相互协作,以实现更高效的并发任务处理。

如何创建一个线程?

在C++中,可以使用std::thread类来创建一个新线程。具体做法是:定义一个函数,然后用std::thread去创建一个线程。
在创建一个新的线程时,需要注意不同线程之间共享的数据和资源,需要正确管理线程间的互斥和同步。

什么是线程安全?

线程安全是指,在多线程程序中,保证共享数据在各个线程中访问和修改的正确性。线程安全的实现可以通过加锁、使用原子操作、避免共享数据等方式来实现。
在C++中,有很多标准库函数和数据结构都是线程安全的,可以直接在多线程程序中使用。同时也需要注意使用互斥锁、条件变量等机制来确保自己写的代码也是线程安全的。

死锁是怎么产生的,如何避免死锁?

死锁是导致线程卡死的锁冲突,简单来说就是两个或者两个以上的线程在执行的过程中争夺同一个共享资源造成的相互等待的现象。
产生死锁有4个必要条件,同时满足这4个条件,死锁才会发生:
1、互斥条件
互斥条件是指多个线程不能同时使用同一个资源,若一个线程已经拥有了该资源,那么其他想获取资源的线程就需要阻塞等待
2、不可剥夺条件:
不可剥夺条件是指当一个资源被线程获取了之后,如果该线程不主动释放该资源,那么该资源一直被占有,其他想获取该资源的线程就要一直进行等待
3、请求与保持条件
请求与保持条件是指线程已经拥有了一个资源,但又提出了一个新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放
4、循环等待条件
循环等待条件是在死锁发生的时候,两个线程获取资源的顺序构成了环形链,环路中每一个线程所占有的资源同时被另一个资源线程申请,也就是前一个线程所占有后一个线程所申请的资源。
要避免死锁问题,一般方法是打破循环等待条件。让两个线程之间获取资源的顺序不要穿插在一起,可以让一个线程现货的两个资源,使用完毕后释放。另一个线程再获取这两个资源,就可以保证两个线程都能正常执行。

互斥锁、自旋锁和读写锁的区别是什么?

互斥锁(Mutex):互斥锁保证在任意时刻只有一个线程能够进入被保护的临界区。当一个线程获取到互斥锁后,其他线程若要进入临界区会被阻塞,直到该线程释放锁。互斥锁是一种阻塞锁,当线程无法获取到锁时,会进入阻塞状态。
自旋锁(Spinlock):自旋锁是一种忙等待锁,当一个线程发现自旋锁被其他线程占用时,它会一直循环等待而不进入阻塞状态,直到该自旋锁可用。自旋锁是一种非阻塞锁,线程在等待锁期间会一直占用 CPU 资源进行循环检测。
读写锁(ReadWrite Lock):读写锁允许多个线程同时对共享数据进行读操作,但只允许单个线程进行写操作。当有线程正在写入时,其他线程无法进行读操作,防止数据不一致性。读写锁允许多个线程并发读,但只能允许单个线程进行写操作。写操作时需要独占锁,阻塞其他线程的读写操作。
应用场景上
互斥锁适用于临界区资源访问时间较长或存在阻塞操作的情况
自旋锁适用于临界区资源访问时间短,且线程竞争不激烈的情况
读写锁适用于读操作远远多于写操作的场景,可以提高并发读性能

线程间同步方式

同一进程内存的多个线程共享同一地址空间。
为了避免多个线程同时访问数据造成的混乱,需要考虑线程之间的同步问题。
所谓同步,即协同步调。按预定的先后次序访问共享资源,以免造成混乱。
线程同步的实现方式主要有6种:
互斥锁、自旋锁、读写锁、条件变量、屏障、信号量

互斥锁在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量进行解锁。对互斥量加锁以后,任何其他试图再次加锁的线程都会被阻塞,直至当前线程释放该互斥量

自旋锁与互斥量类似,但它不使线程进入阻塞态,而是在获取锁之前一直占用cpu,处于忙等自旋状态,自旋锁适用于锁被持有的时间短,且线程不希望在重新调度上话费太多成本的情况。

读写锁有三种状态:读模式加锁,写模式加锁和不加锁
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。读写锁非常适合对数据结构读的次数远大于写的情况
条件变量允许线程睡眠,直到满足某种条件,当满足条件时,可以向该线程发送信号。通知并唤醒该线程,条件变量通常与互斥量配合一起使用。条件变量与互斥量保护,线程在改变条件状态之前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到条件的改变,因为必须在锁住互斥量之后它才可以计算条件是否发生变化。

屏障是用户协调多个线程并行工作的同步机制,屏障允许每个线程等待,直到所有的合作线程都到达某一点(满足条件),然后从该点继续执行。

信号量本质是一个计数器,用于为多个进程提供共享数据对象的访问。编程时可根据操作信号量值的结果,判断是否对公共资源具有访问的权限。当信号量值大于0时,则可以访问,否则将阻塞。PV原语是对信号量的操作,一次P操作使信号量减1,一次V操作使用信号量加1。

进程间通信的6种方式

管道、消息队列、共享内存、信号量、信号和套接字 或者 文件映射

管道分为匿名管道和命名管道,它是一种半双通的通信方式,数据只能单向流动,管道的通信数据遵循先进先出的原则;
匿名管道只能用在父子进程之间传输数据;命名管道可以在不相关进程间通信;
管道的通讯效率低,不适合进程间频繁的交换数据。

消息队列是保存在内核中的消息链表,按照消息的类型进行消息传递,具有较高的可靠性和稳定性。消息队列的消息体有一个最大长度的限制,所以不适合比较大的数据的传输。
消息队列通信过程中,存在用户态和内核态之间的数据拷贝开销。

共享内存是映射一段能被其他进程所访问的内存,这段内存由一个进程创建,但是多个进程都可以访问;
共享内存不需要陷入内核态或系统调用,大大提示了通信的速度,是最快的进程间通信方式。
但是当多进程竞争一个共享资源时,使用共享内存会造成数据错乱的问题。

信号量是一个计数器,可以用来控制多个进程对共享资源的访问,它常作为一种锁机制,与共享内存结合起来使用。用来实现进程和线程对临界区的同步及互斥访问。

信号是一种异步通信机制,用于通知接收进程某个事件已经发生。
例如linux的kill命令就是给进程发送信号。

套接字不仅可以用于不同主机间的进程间通信,也可以用于本地主机上进程间通信。
Socket 通信是基于TCP/IP 网络层上的一种传送方式,我们通常把TCP和UDP称为传输层。

中断和异常的区别

中断是指cpu对系统发生的某事件时的一种响应,即CPU暂停正在执行的程序,在保留现场后,自动地转去执行该事件的中断处理程序, 执行完后,再返回到原程序的断点处继续执行。中断分为外中断和内中断
外中断就是我们指的中断,是指由于外部设备事件所引起的中断,例如磁盘中断,打印机中断等。中断由外因引起,现行指令无关,是正在运行的程序所不期望的,中断的引入是为了支持cpu和设备之间的并行操作。
内中断就是异常,是指由于cpu内部事件所引起的中断,比如程序的非法指令或者地址越界,异常是由cpu本身原因引起,表示cpu执行指令时本身出现的问题。
中断会使cpu用户态变为内核态,使操作系统重新夺回对cpu的控制权。中断是让操作系统夺回cpu使用权的唯一途径。如果没有中断机制,那么一但应用程序上的cpu运行,cpu就会一直运行这个应用程序,不同的中断信号,需要不同的中断处理程序来处理,当cpu检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中存放的位置。

堆内存和栈内存有什么区别?

栈(Stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆(Heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。相比栈来说,堆的使用是灵活的,但也相对复杂。
管理方式:堆内存由程序员管理,需要手动申请和释放;栈内存由编译器自动管理,无需手动操作。
生存期:栈内存中的变量在函数执行完后会自动释放;堆内存中的变量需要程序员手动释放,否则只有在程序运行结束后才会被操作系统回收。
空间大小:栈内存的大小通常比较小,一般只有几MB;堆内存的大小远大于栈内存,通常可以达到GB级别。
碎片问题:频繁的新建、删除堆内存中的数据会造成内存空间的不连续,产生内存碎片;栈内存由于是由系统连续分配,因此不会产生内存碎片。
分配效率:栈内存的分配效率高于堆内存,因为操作系统仅需要移动栈顶指针,而堆内存的分配则需要在内存中寻找足够大的可用空间。
存储内容:栈内存主要存储局部变量、函数参数等;堆内存用于存储需要长时间存在或者大小在运行时决定的数据。
总的来说,两者各有优缺点,使用时应视实际需要选择合适的存储方式。

TCP和UDP的区别是什么?

TCP(传输控制协议)和UDP(用户数据报协议)是在网络传输中常用的两个基于IP协议的传输层协议。
TCP是一种面向连接的协议,通过建立可靠的连接来传输数据。而UDP是一种无连接的协议,数据包发送之前不需要建立连接。
TCP提供可靠的数据传输,它使用序号、确认和重传机制来确保数据的完整性和可靠性。UDP不提供可靠性保证,数据包发送后不能得到确认或重传。

UDP相对于TCP更加轻量级,没有TCP的连接建立和确认过程,因此传输数据速度更快。
TCP需要维护连接状态和传输控制信息,因此消耗的系统资源较多。UDP则简单高效,消耗的系统资源较少。
应用场景上, TCP适用于对可靠性要求较高的应用,如文件传输、电子邮件、网页浏览等。TCP在数据传输过程中能够确保数据的顺序和完整性,适合处理大量的数据和事务类工作。UDP适用于对实时性要求较高、但对可靠性要求不高的应用,如音频/视频流媒体、在线游戏等。UDP的高速传输和低延迟特性对实时性要求高的应用非常有利。

远程线程注入

写一个DLL,让windows自带的计算器加载这个DLL。
需要获取计算器中LoadLibrary的地址。
从xp到win10有一些系统DLL,例如ntdll.dll、kernel32.dll、kernelbase.dll在同一台电脑上的不同进程中的地址模块基址是一样的。
所以想拿计算器的LoadLibrary地址,只要拿LoadLibrary在自己进程的地址就可以了。因为LoadLibrary是kernel32.dll这个DLL中的。
计算器的进程句柄需要OpenProcess拿到,但是OpenProcess需要进程的ID,ID可以通过GetWindowThreadProcessId获取,GetWindowThreadProcessId又需要窗口句柄,所以需要先去拿计算机的窗口句柄。通过FindWindow函数去拿窗口句柄。

钩子hook

局部钩子:局部钩子只能看自己进程的消息。
全局钩子:全局钩子,能看系统范围内的所有消息。
SetWindowsHookEx:设置消息钩子。
UnhookWindowsHookEx :卸载钩子。

调试器:

调试框架

1、建立调试会话
1、CreateProcess(创建一个新的进程)
2、DebugActiveProcess(附加一个已经打开的进程)
3、DebugActiveProcessStop(脱离调试,脱离之前先调用DebugSetProcessKillOnExit,不然被调试的程序会被关闭)
2、循环接受调试事件
1、WaitForDebugEvent
3、处理调试事件
4、提交处理结果
1、ContinueDebugEvent

单步步入:逢call则入 - t命令,TF置位
单步步过:逢call则过 - p命令,非call指令置TF位,call指令在第一条指令设置断点

软件断点:

1、断的下来:指定位置写入CC
2、走的过去:还原原指令机器码,同时置单步(TF)
3、下次还来:单步异常中,再次写入CC

内存断点:

断的下来
1、调试器整体的框架就是基于异常的,想断下来,在访问这块内存的时候,让它抛个异常。2、修改内存属性,让内存不可访问。就会抛异常。
3、修改内存属性,会修改整个分页的。非设置断点的内存被访问的时候,需要放行,让它正常执行。
4、断点来的时候,需要判断类型。是不是自己设置的
走的过去
使用断步配合
1、还原内存属性,设置单步
下次还来
1、在单步中重新设置断点(修改内存属性)

硬件断点:

用汇编特权指令造成异常

反调试:

TF置位被检查出来的几率很大,硬件断点也容易被检测出来

判断是否有硬件断点?
LOCAL @cxt:CONTEXT ;当前线程的上下文信息
使用GetThreadContext获取CONTEXT,判断调试寄存器是否被设置

1、数据检测:对基础的游戏数据进行校验,例如坐标是否违规越界地图(坐标瞬移功能),人物短时间位移距离是否过大(人物加速功能)等等;
2、CRC检测:基于游戏程序代码的检验,例如将人物移动中判断障碍物的je条件跳转修改为jmp强制跳转(人物穿墙功能)等等;
3、封包检测:将游戏数据封包进行校验,防止利用封包漏洞实现违规操作,例如之前的穿X火线强登(可以登录任意账号)等等;
4、机器检测:现在鹅厂 安全组好像换人了 ,游戏机器码封的都挺狠,一封就十年,不过道高一尺,魔高一丈,目前依然不够完善,很多朋友还是可以Pass;
5、Call检测:非法调用Call导致校验值非法,例如攻击Call的严格校验(角色扮演游戏自动打怪脚本都是调用Call的)等等;
6、堆栈检测:该检测归于调用Call过程中产生的问题;
7、文件检测:对于游戏本地文件的检测,例如之前穿X火线几年前风靡一时的REZ文件(快刀秒杀,穿墙,遁地,飞天)等等;
8、模块检测:很多外x挂采用“注入”的形式,所以模块检测在游戏安全对抗中也扮演着极其重要的作用;
9、特征检测:这个主要检测典型的使用“易语言”开发的程序,或者部分外x挂市场比较大的毒瘤程序,或者菜单绘制(imgui绘制)等等;
10、调试检测:针对调试器和调试行为的检测,对Ollydbg,CheatEngine等调试器特征和调试行为的检测等;
11、游戏保护:主要是利用R3各种反调试技术以及驱动层的HOOK等技术实现的游戏保护,例如鹅厂的TP等等。

LoadPE

好处:
1、可以隐藏进程。
2、不用跨进程读写内存了。
代替系统加载PE。

exe程序是一个磁盘上的文件,双击exe程序的时候,系统会为它分配内存空间,然后读PE文件的节表,按照节表先把PE头映射到内存中,然后再把节表的数据映射到内存,再然后根据导入表拿到导入函数的地址保存到IAT中。这样exe程序就可以正常运行了。

LoadPE就是写一个我们自己的程序,程序启动以后,把磁盘上PE文件的PE头、节表、导入表都给拷贝到自己的进程中,最后跳转到它的OEP。这个exe程序就可以在我们自己的进程中运行了。

怎么避免被LoadPE?
GetMoudleFileName,获取模块名判断是不是自己的。
也可以拿模块路径,通过模块路径拿资源。getModulePath。
解决上面判断的方法:↓
把需要加载的原程序改成其他的,把LoadPE改成原程序。

对PE文件做MD5校验,就可以避免被LoadPE。

节表注入

节表注入就是利用节表注入代码。可以使用节间隙、添加节、扩展最后一个节

导出表

dll也是可执行文件,dll中的函数是可以给别的程序使用的,这些函数就会导出。也叫做导出函数。一个dll中的函数或变量并不是全部都要导出的,需要导出的函数或变量,和它们的地址,都保存在导出表。

导入表

INT(import Name Table)导入名称表
IAT(Import Address Table)导入地址表
先遍历导入表,获取dll,加载到进程中,然后遍历它的INT(导入名称表),把函数的地址放到IAT(导入地址表)中。

重定位表

别名:基址重定位表
修改OEP时,可以用到重定位表。
如果开启了随机基址,就有重定位表。没开就没有。
一般dll都有重定位表,exe看它的编译选项。

dump

脱壳的时候就需要用到dump技术。
脱壳:先找入口点,再dump,再修复导入表。

在进程的加载过程中,系统按照节表的结构把PE文件映射到内存。这个进程做完初始化之后就可以运行、执行代码了。
反过来,也可以根据节表的内容把数据从内存中拷贝出来,得出PE文件。(这个就是dump)

准备dump:
在dump的时候找SizeOfHeaders,复制文件偏移0到文件偏移SizeOfHeaders的数据到新的文件中。(SizeOfHeaders的位置在PE(Signature)往后5行半的最后一个dword)
然后再根据NumberOfSections得到节表的数量。再看节表,把它对应的数据拷贝到文件中。

dump异常的问题:
1、如果程序已经运行起来了,再去dump,就会有问题。内存中的数据已经被改了。不是初始值了。
2、如果程序在代码区下了断点,再去dump,也会影响dump。因为下断点也会修改内存中的数据。

总结:dump程序的时候,需要在程序运行之前来dump。(例如在入口点(OEP)断下来的时候)

dump对抗:

1、程序运行起来,把导入表抹除掉,或者做加密/混淆。这样dump下来的程序也不能正常运行。
2、使用loadPE运行程序,dump下来的程序也不能运行。

如果抹除了导入表,现在的工具都可以修复重建。(根据IAT反查)

TLS 表

TLS是Thread Local Storage(线程局部存储)的简称,是一项解决多线程内部变量使用问题的技术。
用于将某些数据和一特定线程关联起来,即,这些数据为关联线程所独有(私有)。
在多线程编程中, 同一个变量, 如果要让多个线程共享访问, 那么这个变量可以使用关键字volatile进行声明;
而如果一个变量不想被多个线程共享访问, 那么就应该使用TLS。
线程局部存储的两种用法:
1、显式TLS
使用Windows提供的API
显式TLS。4个函数:
1、TlsAlloc
2、TlsSetValue
3、TlsGetValue
4、TlsFree
底层是存到teb中的

2、隐式TLS

隐式TLS变量
当我们在代码中申请了tls变量的时候,编译连接器在生成PE文件的时候就会在PE文件中准备一块内存,然后把隐式TLS变量的初值放到内存中。当进程启动的时候,这块内存就会拷贝到PEB中。代码运行的时候还是从peb中拿。

TLS回调函数

允许线程在启动的执行一个回调函数,线程在结束的时候再执行一个回调函数。
所有线程都会执行。

本文标签: 面试题Windows