面试回顾
前言
造成面试糟糕的主因是我自身知识水平有限,面试官都很好,所以没有什么好说的,撸起袖子继续干就完了
问题
远程线程注入
远程线程注入不只是写了一两遍的玩意,没想到被问到流程的时候还是惨不忍睹,下面就重新记录一遍吧(这次一定要记住,加油),流程如下:
-
打开目标进程,获取目标进程句柄
-
在目标进程中申请空间
-
将要注入的
Dll
路径写入刚申请的空间中 -
获取
LoadLibrary
函数地址 -
在目标进程中创建线程,线程回调函数就是
LoadLibrary
函数,回调函数的参数就是要注入的dll
路径 -
等待线程结束
-
清理环境
至于远程线程注入失败的原因,有如下几点(如有不足,不吝赐教):
-
会话(Session)隔离
由于从Windows Vista 开始,微软为了增强系统的安全性,对系统服务和登录用户进行了会话(Session)隔离,系统服务属于会话0,登录的第一个用户属于会话1,之前系统服务和第一个登录用户都属于会话0。此方法并不能突破SESSION 0隔离。原因是在
CreateRemoteThread
函数中对此进行了检查,如果不在同一会话中,调用CsrClientCallServer
为新进程进行登记的操作就会失败,这就直接导致了线程创建的失败。 -
dll
与待注入重新的版本不同(32位与64位) -
最后复习下远程线程注入的原理:
远程线程注入是使用关键函数CreateRemoteThread+LoadLibrary
在其他进程空间中创建一个线程 并且执DLL
中的代码 因为CreateRemoteThread
需要传一个函数地址和多线程参数 而LoadLibrary
正好只有一个参数 所以只要将CreateRemoteThread+LoadLibrary
结合 就能在目标进程注入DLL
了
关键在于获取目标进程中某个DLL
路径字符串的地址和LoadLibrary
的地址
关于LoadLibrary
的地址 虽然windows引入了随机基址的安全机制 但是系统的DLL
的加载基址是固定的 所以Kernel32.dll
里的LoadLibrary
和自己程序空间的地址是一样的
关于DLL
路径字符串的地址 可以直接调用VirtualAllocEx
在目标进程空间申请内存 再调用WriteProcessMemory
函数,将指定的DLL
路径写入目标进程空间就可以了
dll
动态加载与静态加载
回答的时候将其与动态库静态库的概念混淆了,那么就对两者都进行个复习
静态库与动态库
要了解静态库与动态库的区别,先了解什么是库。库说白了就是一段编译好的二进制代码,加上头文件供别人调用。一般用于下面两种场景:
-
需要将某些功能代码供别人使用,但又不希望别人看到源码,就可以以库的形式进行封装,只暴露出头文件供调用
-
对于一些不会进行大的改动的代码,可以打包成库,减少编译时间
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在目标程序编译链接的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。
而动态库即动态链接库(Windows 下的 .dll
,Linux 下的 .so,Mac 下的 .dylib
)。与静态库相反,动态库在目标程序编译链接时并不会将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程
dll
动态加载与静态加载
静态加载又称静态调用也称隐式调用,大部分工作由操作系统完成,使用的时候只要包含对应头文件与对应的库文件
动态加载又称动态调用也称显示调用,通过windows``API
函数加载和卸载dll
,主要函数如下:
-
LoadLibrary
加载dll
-
GetProcAddress
获取要调用的函数的地址 -
FreeLibrary
释放dll
虚函数表与虚基类表
基类与派生类是否共用虚函数表?当时印象中不是,最后左右摇摆中说了我不是很确定。其实提到虚函数表,会想到虚函数表指针,然后想到虚基类表指针,然后就又混淆了
虚函数表
-
对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。
-
对于单继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据(本类有自己的虚函数就添加到拷贝过来的虚函数表的后面)。
-
如果重写了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖
-
对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重写了某个父类的某个虚函数,那么将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。
-
当对象的虚函数被调用时,编译器去查询对象的虚函数表,找到该函数,然后调用
虚基类表
-
如果是虚继承的类对象,那么先存放虚基类表指针,然后存放自己的数据成员,如果同时还有虚函数,则先存放虚函数表指针,再存放虚基类表指针,最后存放自己的数据成员
-
虚基类表第一项为当前子对象相对于虚基类表指针的偏移,第二项为继承的虚基类数据相对于虚基类表指针的偏移
参考:https://blog.csdn.net/fenxinzi557/article/details/51995911
https://blog.csdn.net/qq_41689072/article/details/81350984
APIHOOK
APIhook
基本原理就是修改系统API
函数的调用时机,让别人调用API
时,会先执行我们自定义的函数,然后再执行API
函数。
实现APIhook
的方法如下:
-
inline-hook
:通过修改API
函数体内前五个字节为jmp xxx
(不一定要是前五个字节),跳转到自定义的函数去执行 -
iat-hook
:通过修改导入函数地址表中的API
函数地址为自定义的函数地址
TCP三次握手四次挥手
三次握手
-
TCP服务器进程先创建传输控制块
TCB
,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态; -
TCP客户进程也是先创建传输控制块
TCB
,然后向服务器发出连接请求报文,这是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。 -
TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该
ACK=1
,SYN=1,确认号是ack=x+1
,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD
(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。 -
TCP客户进程收到确认后,还要向服务器给出确认。确认报文的
ACK=1
,ack=y+1
,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK
报文段可以携带数据,但是如果不携带数据则不消耗序号。 -
当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
四次挥手
-
客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
-
服务器收到连接释放报文,发出确认报文,
ACK=1
,ack=u+1
,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。 -
客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
-
服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,
ack=u+1
,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK
(最后确认)状态,等待客户端的确认。 -
客户端收到服务器的连接释放报文后,必须发出确认,
ACK=1
,ack=w+1
,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL
(最长报文段寿命)的时间后,当客户端撤销相应的TCB
后,才进入CLOSED状态。 -
服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销
TCB
后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些
参考:
https://www.cnblogs.com/kingle-study/p/9480689.html
https://www.cnblogs.com/qdhxhz/archive/2004/01/13/8470997.html
阻塞模式与非阻塞模式
因为我主动提了写过一个聊天器,然后就问了我聊天器通讯使用的是阻塞模式还是非阻塞模式,下来看了看,服务端使用多线程阻塞模式,每个连接上服务器的客户端都会开启一个线程进行通信。客户端同样采用多线程,开辟一个线程用于与服务端通信,接收到消息后通过SendMessage
发送自定义消息将从服务端获取到的数据发送给客户端主窗口进行处理
-
使用socket()函数和
WSASocket()
函数创建套接字时,默认的套接字都是阻塞的 -
通过调用
ioctlsocket()
函数,将该套接字设置为非阻塞模式。Linux下的函数是:fcntl()
. -
并不是所有
Windows Sockets API
以阻塞套接字为参数调用都会发生阻塞。例如,以阻塞模式的套接字为参数调用bind()、listen()函数时,函数会立即返回 -
套接字设置为非阻塞模式后,在调用
Windows Sockets API
函数时,调用函数会立即返回。大多数情况下,这些函数调用都会调用“失败”,并返回WSAEWOULDBLOCK
错误代码。说明请求的操作在调用期间内没有时间完成。通常,应用程序需要重复调用该函数,直到获得成功返回代码、 -
要将套接字设置为非阻塞模式,除了使用
ioctlsocket()
函数之外,还可以使用WSAAsyncselect()
和WSAEventselect()
函数。当调用该函数时,套接字会自动地设置为非阻塞方式
参考:
https://blog.csdn.net/lu1024188315/article/details/77942219
https://www.zhihu.com/question/19732473
单例模式与工厂模式
单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在
单例的实现主要是通过以下两个步骤:
-
将该类的构造方法定义为私有化,这样就无法通过调用该类的构造方法来实例化该类的对象
-
在该类内提供一个该类的静态私有对象
-
在该类内提供一个静态公有方法,通过调用该方法创建或获取其本身的静态私有对象
适用场景:
-
需要生成唯一序列的环境需要频繁实例化然后销毁的对象
-
创建对象时耗时过多或者耗资源过多,但又经常用到的对象
-
方便资源相互通信的环境
优点:
-
在内存中只有一个对象,节省内存空间
-
不需要频繁创建销毁对象,可以提高性能
-
避免对共享资源的多重占用,简化访问
-
为整个系统提供一个全局访问点
缺点:
-
不适用于变化频繁的对象
单例模式实现方式又分为以下两种:
-
饿汉式
类装载的时候就完成了实例化,避免了线程同步问题,简单点说就是程序启动时就创建了一个唯一的实例对象
-
懒汉式
只有在真正使用的时候才会实例化一个对象(第一次使用时才创建一个唯一的实例对象),这种写法起到了延迟加载的效果,但是只能在单线程下使用,如果在多线程下,可能会产生多个实例(可以采用双检锁解决,使用双检锁最好给静态对象加上
volatile
修饰词,禁止指令重排序)
参考:
https://blog.csdn.net/weixin_34248258/article/details/94471399
https://www.cnblogs.com/xuwendong/archive/2004/01/13/9633985.html
https://www.cnblogs.com/xz816111/p/8470048.html
工厂模式
简单工厂模式
结构组成
-
工厂类:工厂模式的核心类,会定义一个用于创建指定的具体实例对象的接口
-
抽象产品类:是所有具体产品类的父类
-
具体产品类:工厂类所创建的对象就是此具体产品实例
特点
工厂类封装了创建具体产品对象的函数
缺陷
扩展性非常差,新增产品的时候,需要修改工厂类。违反了开放封闭原则:软件实体(类、模块、函数)可以扩展,但是不可修改
工厂方法模式
结构组成
-
抽象工厂类:工厂方法模式的核心类,提供创建具体产品的接口,由具体工厂类实现
-
具体工厂类:继承于抽象工厂,实现创建对应具体产品对象的方式
-
抽象产品类:是所有具体产品类的父类
-
具体产品类:具体工厂所创建的对象就是此具体产品实例
特点
-
工厂方法模式抽象出了工厂类,提供创建具体产品的接口,并由子类实现
-
工厂方法模式的应用不仅封装了具体产品对象的创建,而且将具体产品对象的创建放到了子类具体工厂类去实现
缺陷
-
每新增一个产品,就需要增加一个对应的产品的具体工厂类。相比简单工厂模式而言,需要更多的类定义
抽象工厂模式
结构组成
-
抽象工厂类:工厂方法模式的核心类,提供创建具体产品的接口,由具体工厂类实现
-
具体工厂类:继承于抽象工厂,实现创建对应具体产品对象的方式
-
抽象产品类:是具体产品类的父类(多少种产品就有多少个该类)
-
具体产品类:具体工厂所创建的对象就是此具体产品实例
特点
提供一个接口,可以创建多个产品簇中的产品对象。如创建耐克工厂,则可以创建耐克鞋子产品、衣服产品、裤子产品等
缺陷
同工厂方法模式一样,新增产品时,需要增加一个对应的产品的具体工厂类
参考:
https://www.cnblogs.com/xiaolincoding/p/11524376.html
https://blog.csdn.net/u012156116/article/details/80857255
进程间通信
进程间通信主要包括管道、消息队列、信号、共享内存、套接字socket
管道
管道主要包括匿名管道和命名管道。匿名管道用于具有亲缘关系的父子进程间的通信,命名管道允许无亲缘关系进程间的通信
-
普通管道PIPE:
-
它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端
-
它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
-
它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
-
-
命名管道FIFO:
-
FIFO可以在无关的进程之间交换数据
-
FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
-
消息队列
消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标记。 (消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点)具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
特点:
-
消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
-
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
-
消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取
信号量semaphore
信号量(semaphore)与已经介绍过的 IPC
结构不同,它是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
特点:
-
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
-
信号量基于操作系统的
PV
(P操作:申请资源操作。V操作:释放资源操作) 操作,程序对信号量的操作都是原子操作。 -
每次对信号量的
PV
(P操作:申请资源操作。V操作:释放资源操作) 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。 -
支持信号量组
共享内存
它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
特点:
-
共享内存是最快的一种
IPC
,因为进程是直接对内存进行存取 -
因为多个进程可以同时操作,所以需要进行同步
-
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
套接字SOCKET
socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信
参考:
https://www.nowcoder.com/ta/review-c/review?page=79
https://blog.csdn.net/ws_Ando/article/details/83501519
智能指针
智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放
C++智能指针可以很大程度上避免因没有及时释放资源而导致的内存泄露,C++中有四个智能指针:auto_ptr
,shared_ptr
,weak_ptr
,unique_ptr
,其中后三个是C++11支持的,前一个已经被C++11弃用
auto_ptr
不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错,因此存在潜在的内存崩溃问题。因为不能被复制,所以不能被放入容器中
unique_ptr
, 也不支持复制和赋值,但比auto_ptr
好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move
C++11或boost的shared_ptr
,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放
C++11或boost的weak_ptr
,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr
。顾名思义,weak_ptr
是一个弱引用,只引用,不计数。如果一块内存被shared_ptr
和weak_ptr
同时引用,当所有shared_ptr
析构了之后,不管还有没有weak_ptr
引用该内存,内存也会被释放。所以weak_ptr
不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr
是否为空指针
参考:
https://www.cnblogs.com/WindSun/p/11444429.html