0%

SSDT表

SSDT表示系统服务表,我们ring3调用的很多函数都会调用这个SSDT表。这个表就是一个把 Ring3 的 Win32 API 和 Ring0 的内核 API 联系起来。SSDT 并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。通过修改此表的函数地址可以对常用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

1
2
3
4
5
6
7
8
9
10
typedef struct _SERVICE_DESCRIPTOR_TABLE
{
PULONG ServiceTableBase; // 指针,指向函数地址,每个成员占4字节
PULONG ServiceCounterTableBase; // 当前系统服务表被调用的次数
ULONG NumberOfService; // 服务函数的总数
PUCHAR ParamTableBase; // 服务函数的参数总长度,以字节为单位,每个成员占一个字节
// 如:服务函数有两个参数,每个参数占四字节,那么对应参数总长度为8
// 函数地址成员 与 参数总长度成员 一一对应
} SSDTEntry, *PSSDTEntry;

用本地内核调试查看SSDT表,第一个参数指向的地址存储的是全部内核函数

image-20220513101524872

image-20220513101808757

第二个参数代表ssdt表里面有多少个内核函数

image-20220513101659232

第三个参数是一个指针指向一个地址,这里表示的是与上面的内核函数相对应的参数个数,例如第一个为10,参数个数就为0x10/4 = 4

image-20220513101831019

image-20220513101934441

系统中一共存在两个系统服务描述表,KeServiceDescriptorTableKeServiceDescriptorTableShadow,其中 KeServiceDescriptorTable 主要是处理来自 Ring3 层 Kernel32.dll 中的系统调用,而 KeServiceDescriptorTableShadow 则主要处理来自 User32.dll 和 GDI32.dll 中的系统调用,并且KeServiceDescriptorTable 在ntoskrnl.exe(Windows 操作系统内核文件,包括内核和执行体层)是导出的,而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出。

image-20220513102906581

CR0寄存器

SSDT表所在的内存页属性是只读,没有写入的权限,所以需要把该地址设置为可写入,这样才能写入自己的函数,使用的是CR0寄存器关闭只读属性,开关在CR0寄存器的第16位。

image-20220513110615404

image-20220513110650432

可以看到这里使用32位寄存器,而在CR0寄存器中,我们重点关注的是3个标志位:

PE ­ 是否启用保护模式,置1则启用。
PG ­ 是否使用分页模式, 置1则开启分页模式, 此标志置1时, PE 标志也必须置1,否则CPU报异常。
WP WP为1时, 不能修改只读的内存页 , WP为0时, 可以修改只读的内存页。

驱动

原作者写了个简单驱动,其主要代码部分如下,就是替换SSDT表中相应函数地址。自己写的函数地址就是将相应进程设为拒绝访问,最后调用原函数。

image-20220513111120329

image-20220513110923377

不过,x64需要绕过PatchGuard保护,困难较大

参考文章:https://drunkmars.top/2022/02/15/ssdt%20hook/

摘要

可以快速检测和分类不同的勒索软件类别,以制定快速响应方案的勒索软件防御解决方案,一直是近年来的高需求。尽管采用深度学习技术提供自动化和自我学习的适用性已经在许多应用领域得到证明,但缺乏勒索软件(和其他恶意软件)样本的数据已经成为开发有效的基于深度学习的解决方案的障碍。为了解决这个问题,论文提出了一个基于少量元学习的孪生神经网络(孪生网络是一种特殊类型的神经网络架构。与一个学习对其输入进行分类的模型不同,该神经网络是学习在两个输入中进行区分。它学习了两个输入之间的相似之处。),它不仅可以检测勒索软件攻击,而且能够将它们划分为不同的类别。

该篇论文提出的模型利用直接从勒索软件二进制文件中获得的熵特征来保留与不同勒索软件签名相关的更细粒度的特征。这些熵特征被以元学习的方式使用一个预先训练过的网络(比如VGG-16)进一步用于训练和优化模型。与使用特征图像相比,这种方法产生更准确的权重因子,以避免通常与训练样本有限的模型的偏差。实验结果表明,与其他类似的方法相比,该模型的加权f1分数超过86%。

论文贡献

  • 即使在每一类样本数量非常小的情况下,也可以从不同的勒索软件类别中学习到其特征。
  • 由于图像特征在进行转换的时候通常导致信息丢失,该方法采用了熵特征
  • 使用预先训练好的VGG-16网络作为元学习过程的一部分,以生成权重,更准确地捕获每个勒索软件样本的特征。这不仅有助于提高分类精度,而且还避免了与在深度学习模型中使用有限数量的训练样本相关的潜在偏差。
  • 使用了两个中心损失和一个softmax损失的组合,以准确地捕获属于同一类的勒索软件样本之间的相似性(即类内方差)和跨不同勒索软件类的勒索软件样本之间的di-similarity(即类间方差)。
  • 实验对11个不同类别的1046个勒索软件样本进行了测试,结果表明,该模型的加权f1分数超过86%,对不同的勒索软件样本进行了正确分类,优于现有的基准方法。

方法模型

孪生神经网络(SNN)其核心功能是评估由特征嵌入空间表示的两幅图像之间的相似度,并生成相似度评分。一个通用的SNN由两个子网络组成,如图所示,其中的权值和超参数设置被共享。两个子网络都以一个属于同一类的一幅图像(即正对)作为输入和输出它所学习到的特征。将这些特征映射到全连接层的特征嵌入中,同时使用损失函数来预测两个子网络处理后的图像是否属于同一类。它还可以预测学习到的不同特征是否与同一类中的不同实例互补。例如,它可以正确地预测不同的勒索软件实例仍然属于同一个勒索软件家族,因为它们共享唯一的勒索软件签名。

image-20220513170335827

熵特征

熵特征是直接从二进制勒索软件文件中计算出来的。图像特征很容易被白噪声和复杂纹理特征影响,熵特征则不会。并且论文对基于熵特征和灰度图的模型计算了训练时间,来比较计算开销。发现基于熵特征的模型训练时间要少十分钟,并且前者基于新测试样本的执行时间要少两秒。

模型

image-20220513172428996

每个熵图的熵值被输入到SNN的每个子网络,在每个子网络中,采用一个预训练的VGG-16模型(该模型权重和参数都在ImageNet熵训练的),并以元学习的方式使用它。(预训练的模型有助于提出的模型的训练)

总结

虽然所提出的模型总体上表现良好,但它有几个局限性。首先,该模型仍然不能对Bitman勒索软件家族或Teslacrtpt勒索软件家族进行分类。其原因如图所示,在训练后,Bitman和Teslacrtpt家族的集群仍然重叠。没有一个围绕这两个家族的一个明确的集群,该模型倾向于产生mi-classification结果,特别是跨属于这两个家族的恶意软件实例。

image-20220516143653869

其次,提出的模型还没有对零日攻击进行测试,而只提供了训练样本中包含的已知攻击的分类结果。最后,与其他方法相比,该模型需要更长的训练时间,因为需要训练的参数数量更多。

字符串-滑动窗口最大值

题目链接

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
class Solution {
private:
class MyQueue{
public:
deque<int> que;
void pop(int val){
if(!que.empty() && que.front() == val)
que.pop_front();
}
void push(int val){
while(!que.empty() && val > que.back()){
que.pop_back();
}
que.push_back(val);
}
int front(){
return que.front();
}
};
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
MyQueue que;
vector<int> res;
for(int i = 0; i < k; i++)que.push(nums[i]);

res.push_back(que.front());
for(int i = k; i < nums.size(); i++){
que.pop(nums[i-k]);
que.push(nums[i]);
res.push_back(que.front());
}
return res;
}
};
1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for item in tokens:
if item not in {"+", "-", "*", "/"}:
stack.append(item)
else:
num1, num2 = stack.pop(), stack.pop()
stack.append(
int(eval(f'{num2} {item} {num1}'))
)
return int(stack.pop())

字符串-有效的括号

题目链接

这道题多分析一下题目就知道是一种消除题,其实和前面一道题“对对碰”很像,这道题是出现运算符就运算前面两个数字,结果又重新压入栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int>st;
for(int i = 0; i < tokens.size(); i++){
if(tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/"){
int num1 = st.top();
st.pop();
int num2 = st.top();
st.pop();
if(tokens[i] == "+")st.push(num2 + num1);
if(tokens[i] == "-")st.push(num2 - num1);
if(tokens[i] == "*")st.push(num2 * num1);
if(tokens[i] == "/")st.push(num2 / num1);
}
else{
st.push(stoi(tokens[i]));
}
}
return st.top();
}
};
1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for item in tokens:
if item not in {"+", "-", "*", "/"}:
stack.append(item)
else:
num1, num2 = stack.pop(), stack.pop()
stack.append(
int(eval(f'{num2} {item} {num1}'))
)
return int(stack.pop())

字符串-有效的括号

题目链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
string removeDuplicates(string S) {
stack<char>st;
for(char s : S){
if(st.empty() || s != st.top())st.push(s);
else{st.pop();}
}

string result = "";
while(!st.empty()){
result += st.top();
st.pop();
}
reverse(result.begin(), result.end());
return result;

}
};

静态分析

首先查看保护,全开,64位动态链接成簇,stripped.

image-20220510143212911

用IDA查看程序,函数逻辑如下,主要有4个功能

image-20220510143738417

image-20220510141350991

allocate

可以看到该功能主要负责分配堆块以及记录堆块信息。

image-20220510152759187

image-20220510153445247

fill

该功能先读取索引,然后判断堆块第一位如果是1,那么就输入size,content.这里可以向堆块输入任意长度,造成溢出

image-20220510160751315

image-20220510161522247

Free

image-20220510161738040

dump

image-20220510162009877

利用分析

想要利用,首先是要泄露libc地址。这里采用的是泄露unsortbin链表的地址,先申请四个堆块如图,其中最后一个chunk 3的作用是和top chunk隔离(申请0x68的大小实际上就是0x70):

image-20220510170059939

然后利用堆溢出,堆chunk 0编辑,使chunk 0溢出内容将chunk1 的size修改为0xe1(包含pre_size),即chunk1+chunk2大小的和,然后pre占用位为1,这时释放1,相当于释放了一个size为0xe0的chunk,会被放入unsortbin链表中。(图中可以看到有tcache,一种内存管理机制,代码逻辑位于malloc函数和free函数中,具有较高的优先级。属于一种缓存机制,它为每一个线程创建了一个缓存,从而实现了不加锁的堆块分配算法,起到了性能提升的作用。)

image-20220511095326202

这时再申请一个0x70的堆块,便会从unsortbin中分割一个0x70的大小的堆块出来,然后剩下的继续连入unsortbin中。

但需要注意的是,分割之前unsortbin中的那个0xe0大小的堆块是我们通过溢出伪造的堆块,实际上是由一个0x70的(已释放堆块,在前)和一个0x70的(未释放堆块,在后)组成的。这时再申请一个0x70的堆块就会将前一部分已释放的那个堆块重新申请回来,那么后一个堆块就会被作为空闲堆块连入unsortbin中,但实际上这个堆块我们可控,我们可以将其内容输出,就会获得一个指向unsortbin的指针值,便泄露了libc地址。

然后将malloc_hook修改为onegadget,然后申请一个堆块就会完成getshell。

这部分的操作就是,只需要再盛情一个0x70的堆块chunk4,就会发现我们有两个堆块(chunk2和chunk4)同时指向同一个0x70的地址空间。这时我们只需释放chunk2,然后编辑chunk4便可修改fastbin的指针了。

这里都是跟的这篇博客走的,但是复现出现了tcache这样的状况。就顺着理解了一下题目的思路。

参考链接:https://www.bilibili.com/video/BV12v4y1o7DK?spm_id_from=333.337.search-card.all.click

知识点

main_arena

chunk空间的共用情况,也就是下一个的chunk的prev_size域给当前chunk当做数据域使用,这种情况只出现在malloc的大小为8的奇数倍的情况。

在正常情况下,当free掉一块大于max_fast的大小的chunk时,程序将会把他放至unsorted bins中,而由于unsorted是双向链表的结构,所以它有FDBK两个指针,且当fastbin为空时,他的fdbk指针将同时指向main_arena中,一般也就是main_arena+88的位置。

main_arena存储在libc.so.6文件的.data段,通过这个偏移我们就可以获取libc的基址。

字符串-有效的括号

题目链接

一开始会想到,左括号的数量和右括号的数量相等,会不会就是有效的括号。其实不然,因为会有[{]}这样的情况出现。仔细分析我们发现,对于有效的括号,它的部分子表达式仍然是有效的括号,比如 {()[()]} 是一个有效的括号,()[{}] 是有效的括号,[()] 也是有效的括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
bool isValid(string s) {
unordered_map<char, int>m{{'(',1},{'[',2},{'{',3},
{')',4},{']',5},{'}',6}};
stack<int> st;
for(char c : s){
int index = m[c];
if(index >= 1 && index <= 3){
st.push(c);
}
else if(!st.empty() && m[st.top()] == index-3){
st.pop();
}
else{
return false;
}

}
return st.empty();
}
};
1
2
3
4
5
6
7
8
9
10
11
class Solution:
def isValid(self, s: str) -> bool:
dic = {')':'(',']':'[','}':'{'}
stack = []
for i in s:
if stack and i in dic:
if stack[-1] == dic[i]: stack.pop()
else: return False
else: stack.append(i)

return not stack

UAF

  • free之后没有清空指针(并没有将该指针置为NULL)
  • 通过读chunk的内容可以泄露leak libc地址或堆地址
  • 通过王chunk写内容可以劫持链表(bin)
  • UAF属于一种漏洞点,需要结合具体利用方式进行利用(譬如fastbin attack等)

lab 10 hacknote

功能分析

程序主要有 3 个功能。之后程序会根据用户的输入执行相应的功能。

image-20220506153130995

image-20220506153144645

add_note

我们可以看出程序最多可以添加 5 个 note。每个 note 有两个字段 print_content与 content,其中 put 会被设置为一个函数,其函数会输出 content 具体的内容。

image-20220506153725159

image-20220506154131927

print_note 就是简单的根据给定的 note 的索引来输出对应索引的 note 的内容。

image-20220506154312521

delete_note

delete_note 会根据给定的索引来释放对应的 note。但是值得注意的是,在 删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 Use After Free 的情况的。

image-20220506154428143

利用分析

注意到前面的print_note_content函数,猜想应该是可以将一个note的print_note字段修改为一个magic函数地址。

image-20220506162229383

image-20220506162254874

显然 note 是一个 fastbin chunk(大小为 16 字节)。我们的目的是希望一个 note 的 print_note字段为 magic 的函数地址,那么我们必须想办法让某个 note 的 print_note 指针被覆盖为 magic 地址。由于程序中只有唯一的地方对 print_note进行赋值。所以我们必须利用写 real content 的时候来进行覆盖。具体采用的思路如下:

  • 申请 note0,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 申请 note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 释放 note0
  • 释放 note1
  • 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  • 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则,note2 其实会分配 note1 对应的内存块。real content 对应的 chunk 其实是 note0。
  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数。

image-20220506161607282

漏洞利用

字符串-用队列实现栈

题目链接

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
class MyStack {
public:
queue<int> que1;
queue<int> que2;
MyStack() {

}

void push(int x) {
que1.push(x);
}

int pop() {
int size = que1.size();
size--;
while(size--){
que2.push(que1.front());//如果用单队列实现,可以直接que1.push(que1.front())
que1.pop();
}

int res = que1.front();
que1.pop();
que1 = que2;
while(!que2.empty())que2.pop();

return res;
}

int top() {
return que1.back();
}

bool empty() {
return que1.empty();
}
};

/**
* Your MyStack object will be instantiated and called as such:
* MyStack* obj = new MyStack();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->top();
* bool param_4 = obj->empty();
*/

字符串-用栈实现队列

题目链接

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
class MyQueue {
public:
stack<int> stIn;
stack<int> stOut;
MyQueue() {

}

void push(int x) {
stIn.push(x);
}

int pop() {
if(stOut.empty()){
while(!stIn.empty()){
stOut.push(stIn.top());
stIn.pop();
}
}
int result = stOut.top();
stOut.pop();
return result;

}

int peek() {
int result = this->pop();//直接复用前面的pop函数,记得pop后要保存
stOut.push(result);
return result;
}

bool empty() {
return stOut.empty() && stIn.empty();
}
};

/**
* Your MyQueue object will be instantiated and called as such:
* MyQueue* obj = new MyQueue();
* obj->push(x);
* int param_2 = obj->pop();
* int param_3 = obj->peek();
* bool param_4 = obj->empty();
*/
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
typedef struct {
int stInTop, stOutTop;
int stIn[100],stOut[100];
} MyQueue;


MyQueue* myQueueCreate() {
MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue));
queue->stInTop = 0;
queue->stOutTop = 0;
return queue;
}

void myQueuePush(MyQueue* obj, int x) {
obj->stIn[(obj->stInTop)++] = x;
}

int myQueuePop(MyQueue* obj) {
int stInTop = obj->stInTop;
int stOutTop = obj->stOutTop;
if(stOutTop == 0){
while(stInTop > 0)obj->stOut[stOutTop++] = obj->stIn[stInTop];
}

int top = obj->stOut[--stOutTop];

while(stOutTop > 0)obj->stIn[stInTop++] = obj->stOut[--stOutTop];

obj->stInTop = stInTop;
obj->stOutTop = stOutTop;

return top;

}

int myQueuePeek(MyQueue* obj) {
return obj->stIn[0];
}

bool myQueueEmpty(MyQueue* obj) {
return obj->stInTop == 0 && obj->stOutTop == 0;
}

void myQueueFree(MyQueue* obj) {
obj->stInTop = 0;
obj->stOutTop = 0;
}

/**
* Your MyQueue struct will be instantiated and called as such:
* MyQueue* obj = myQueueCreate();
* myQueuePush(obj, x);

* int param_2 = myQueuePop(obj);

* int param_3 = myQueuePeek(obj);

* bool param_4 = myQueueEmpty(obj);

* myQueueFree(obj);
*/