作者dirkc (體驗教育即生活)
看板C_and_CPP
標題Re: [問題] shellcode 的 hello world
時間Fri Nov 23 17:27:30 2012
首先想謝謝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