看板 C_and_CPP 關於我們 聯絡資訊
首先想謝謝littleshan熱心回應,我覺得清楚回答這個題目本身滿酷的 我對在Linux上撰寫shellcode沒什麼經驗,但是Windows上面有一些些學習,所以想對於 前文中提到的一些部份分享在Windows上的情況。 我回在內文中。 ※ 引述《littleshan (我要加入劍道社!)》之銘言: : ※ 引述《a613204 (胖胖)》之銘言: : : 大家好 最近想寫shellcode但是遇到很大的問題 : : 開發環境是在Ubuntu 12.10中 : : 對於整個流程不是很清楚 : : 目前我的作法是先寫一個hello.c : : 裡面的程式碼像這樣 : : #include <stdio.h> : : int main(void) : : { : : printf("Hello World"); : : } : : 用gcc -o hello hello.c 編譯完成後 : : 再用objdump -d hello 印出machine code : : 但是我不曉得要把那些machine code放到 char shellcode[] = "..your shellcode.." : : 有沒有人可以告訴我完整的流程?? 卡在這邊滿久的 感恩 : 我不知道你是看哪本書學來的方法 : 寫 shellcode 大概有幾個要注意的地方 : 1. 你不能呼叫 standard library : 因為那些 function 的位址都是 link 後才知道的 : 但 shellcode 沒辦法和主程式 link : 所以你也不知道 printf 的位址是什麼 據我經驗在Windows下Standard Library是可以被呼叫的 但是要知道被呼叫的函式是在哪一個DLL檔案裡面,舉例來說預先知道printf在msvcrt.dll裡面 我可以透過kernel32.dll載入系統函式LoadLibraryA,再透過LoadLibraryA把msvcrt.dll載入到記憶體中 再從msvcrt.dll抓到printf的相對位址,來呼叫printf函式。 當然,以上都是假設ASLR沒有啟動的情況下,例如VC++的Linker沒有加入/DYNAMICBASE參數的舊專案 或者是美好的Windows XP環境 以下是一個透過kernel32.dll抓取LoadLibraryA,再載入msvcrt.dll,然後找到printf以及exit兩個函式, 並且對其呼叫的例子,環境為 Windows XP SP3,編譯環境為VC++ 2010。 這個shellcode是我自己寫的,我不建議有任何人執行此範例,列在這裡只是單純舉例說明 動態載入程式庫然後呼叫printf在Windows下辦得到,事實上這種方式可能是在Windows寫shellcode的常態。 #include <windows.h> #include <cstdio> using namespace std; //Size: 195 bytes char shellcode[] = "\xeb\x4e\x60\x8b\x6c\x24\x24\x8b\x45\x3c\x8b\x54\x05\x78\x01\xea" "\x8b\x4a\x18\x8b\x5a\x20\x01\xeb\xe3\x34\x49\x8b\x34\x8b\x01\xee" "\x31\xff\x31\xc0\xfc\xac\x84\xc0\x74\x07\xc1\xcf\x0d\x01\xc7\xeb" "\xf4\x3b\x7c\x24\x28\x75\xe1\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b" "\x8b\x5a\x1c\x01\xeb\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3" "\x31\xc0\x64\x8b\x58\x30\x8b\x5b\x0c\x83\xc3\x1c\x8b\x1b\x8b\x4b" "\x08\x8b\x53\x20\x38\x42\x18\x75\xf3\x68\x8e\x4e\x0e\xec\x51\xe8" "\x8e\xff\xff\xff\x68\x6c\x6c\x00\x00\x68\x72\x74\x2e\x64\x68\x6d" "\x73\x76\x63\x54\xff\xd0\x68\x1e\x3c\xa7\xd5\x50\xe8\x71\xff\xff" "\xff\x89\xc1\xc7\x44\x24\x04\x74\x1e\x48\xcd\xe8\x62\xff\xff\xff" "\x89\xc2\x68\x21\x0a\x00\x00\x68\x6f\x72\x6c\x64\x68\x6f\x2c\x20" "\x57\x68\x48\x65\x6c\x6c\x89\xe6\x31\xc0\x50\x52\x56\xff\xd1\x5a" "\x5a\xff\xd2"; //NULL count: 4 typedef void (*FUNCPTR)(); int main() { printf("<< Shellcode 開始執行 >>\n"); unsigned dummy; VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, (PDWORD)&dummy); FUNCPTR fp = (FUNCPTR)(void*)shellcode; fp(); printf("(你看不到這一行,因為 shellcode 執行 exit() 離開程式了)"); } : 2. 一般程式編譯後會有 text section 與 data section : 前者放機器碼後者放預先設定好的變數內容 : 比如說你的 "Hello World" 這個字串會存在 data section : 但是 shellcode 是一整塊程式碼 : 你可以想象成只有 text section 而沒有 data section 能用 : 所以為了宣告並取得一塊內容是 "Hello World" 的記憶體 : 你必需透過一些小手段 : 最常見的手段是用 call 這個指令 這方法超讚的,我提供另外一個想法就是直接PUSH字串到堆疊裡面,例如: push 0x00000A21 push 0x646C726F push 0x57202C6F push 0x6C6C6548 Hello, World!\n的16進位如下: H e l l o , W o r l d ! \n 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A 考慮到Windows的little-endian,我們如果倒過來看就是我上面貼的push組語。 : 以下就是 hello world 的 shellcode 範例 : 為了簡化問題我用的是 32bit x86 : BITS 32 : call main : db "hello world", 10 : main: : mov eax, 4 : mov ebx, 0 : pop ecx : mov edx, 12 : int 0x80 : mov eax, 1 : mov ebx, 0 : int 0x80 : 首先是 call 這個指令 : x86 指令的 call 有個神奇的特性,它可以吃一個相對位址 : 因為 main 這個地方的位址與 call 的位址差距是在組譯時就可以算出來的 : 所以這個 call 就相當於 near jmp 可以直接跳到 main 繼續執行 : 但這邊放 call 是有原因的 : 因為 call 會把它的 return address 送進 stack 中 : 而 return address 是什麼呢?就是它下一個指令的位址 : 也就是 "hello world" 這段資料的開頭 : 取得字串的位址後,接下來我們要把它印出來 : 我們沒辦法呼叫 printf,所以我們要去實作出 printf 的部份功能 : 也就是使用 system call 來輸出字串 : Linux 上要輸出資料,使用的是 write 這個 system call : ssize_t write(int fd, const void *buf, size_t count); : Linux 上呼叫 system call 有它固定的規則: : * eax 中填入 system call 的代碼,write 的代碼是 4 : * ebx 填入第一個參數,我們要輸出至 standard output 所以填 0 : * ecx 填第二個參數,我們用 pop ecx 把 stack 中指向字串的指標放進 ecx : * edx 填第三個參數,這是 12 也就是字串長度加上後面的換行 : 填好參數後使用 0x80 interrupt 就會呼叫 system call 了 : 後面那三行則是離開程式,也就是呼叫 exit(0) 這個 system call : 然後我們就可以來執行看看這個 shellcode : 首先用 nasm 來組譯 : ~$ nasm -o hello hello.asm : 然後把它轉成 C code : 這邊就要提一下,有個工具叫 xxd 非常好用滴 : ~$ xxd -i hello : unsigned char hello[] = { : 0xe8, 0x0c, 0x00, 0x00, 0x00, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, : 0x6f, 0x72, 0x6c, 0x64, 0x0a, 0x59, 0xb8, 0x04, 0x00, 0x00, 0x00, 0xbb, : 0x00, 0x00, 0x00, 0x00, 0xba, 0x0c, 0x00, 0x00, 0x00, 0xcd, 0x80, 0xb8, : 0x01, 0x00, 0x00, 0x00, 0xbb, 0x00, 0x00, 0x00, 0x00, 0xcd, 0x80 : }; : unsigned int hello_len = 47; : 然後把它放進你的程式碼: : unsigned char hello[] = {...}; : int main(void){ : typedef void (*FPTR)(void); : FPTR shellcode = (FPTR)hello; : (*shellcode)(); : return 0; : } : 試著編譯執行看看 : ~$ gcc -m32 hello.c : ~$ ./a.out : Segmentation fault : 然後就當掉了哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈 : 之所以會當掉,是因為現在的 OS 都有 NX bit 的功能 : 只有 text section 中的資料才能當作程式碼執行 : 上面的宣告預設會把 hello 這個陣列放在 data section : 所以一執行馬上就被 OS 擋下來了 : 解法有兩種,一種是用 __attribute__ 把它改到 text section : 這個方法板上有人提過了 : 另一個方法是編譯的時候加上參數 -z execstack : 會讓這個執行檔免於 OS 的限制 : ~$ gcc -m32 -z execstack hello.c : ~$ ./a.out : hello world : 玩 shellcode 要對 assembly 及 system programming 有完整理解 : 若你有不懂的地方,就先看看這部份的書 : 另外,這段 shellcode 有個缺點 : 就是它裡面包含了太多 0x00 : 大家都知道 0x00 就是 null character 也就是 C string 的尾巴 : 因此這塊 shellcode 沒辦法用 strcpy 之類的函式作複製 : 所以你也很難把它拿去做壞事 : 避免 shellcode 中出現 null 是可能的 : 但同樣地你要先學好 assembly 完全同意,避免出現null的想法我也提一個,就是可以透過編碼器(英文是說encoder,當然這字常被在各處使用) : 另外之二 : : → a613204:有人可以講解一下嗎QQ : : → purincess:前一頁的不是也是原po嗎 XD : : → a613204:沒人知道該怎麼寫嗎 : 在討論區問問題 : 這句話是大忌 : 這個板上比我強的人兩隻手數不完 : 但看到這行 大概會覺得回去打LoL比較實在 LOL :) -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 111.249.204.18
littleshan:感謝分享,Linux上我覺得應該也有方法,不過還沒研究! 11/24 01:04
cobrasgo:windows上我也不知道要怎麼搞,先推再看 11/24 11:51
UNARYvvv:可以研究看看 return-oriented programming 11/25 05:46