如果純粹用文字來描述什麼是動態注入,可能還是不太容易理解,所以本篇文章從如下一段程式碼開始:
// who.c
#include <stdio.h>
#include <unistd.h>
int main()
{
while (1) {
printf("who are you ?\n");
sleep(2);
}
return 0;
}
這段程式碼本身沒有什麼意思,執行"gcc who.c -o who -g -Wall"完成編譯並啟動who程式,每隔2s會向終端列印一句"who are you ?",但有意思的是,可能會出現一種"詭異"的現象,比如螢幕上突然冒出一句"it's me ~"。
1. 為什麼可以出現這種“詭異”的現象?
通過反彙編who程式可以看出,程式最終會進入while(1)迴圈體,重複執行"bf24064000"、"e8c5feffff"、"bd0x000000"、"e8ebfeffff"、"ebea"這5條機器指令:
如果who程式執行到某條指令(比如"e8c5feffff")時暫停了,並且在恢復執行之前,0x400586處的指令塊被替換成列印"it's me ~"的程式碼,那麼who程式下次執行,自然就會列印"it's me ~"。
2. 怎麼獲取列印"it's me ~"的機器碼?
// isme32.c
int main()
{
__asm__(
"jmp forward\n\t"
"backward:popl %esi\n\t"
"movl $4, %eax\n\t"
"movl $2, %ebx\n\t"
"movl %esi, %ecx\n\t"
"movl $12, %edx\n\t"
"int $0x80\n\t"
"int3\n\t"
"forward:call backward\n\t"
".string \"it's me ~\\n\""
);
return 0;
}
// isme64.c
int main()
{
__asm__(
"jmp forward\n\t"
"backward:popq %rsi\n\t"
"movq $1, %rax\n\t"
"movq $2, %rdi\n\t"
"movq $12, %rdx\n\t"
"syscall\n\t"
"int3\n\t"
"forward:call backward\n\t"
".string \"it's me ~\\n\""
);
return 0;
}
本篇文章的實驗環境是64位ubuntu系統,所以執行"gcc isme64.c -oisme64 -g -Wall"生成isme64可執行檔案,並通過反彙編isme64檔案提取機器碼:
用藍色、綠色標記出來的16進位制內容,即為列印"it's me ~"的機器碼,但有2點需要說明:
- 綠色部分其實是"it's me ~\n\0"這串字元的ascii碼值,也被objdump解釋成彙編指令了,這是反彙編工具很難避免的一個問題,用gdb/disassemble反彙編的結果也是一樣的,因為它們選擇的都是線性掃描演算法,遞迴下降演算法效果會好一些,但仍然不能保證得到精確的結果;
- C程式實現列印"it's me ~",寫一條printf("it's
me ~")語句不就可以了麼,為什麼要寫成這種"奇奇怪怪"的樣子?因為如果用C語法來實現的話,"it's me
~"這個字串會被編譯器安排到isme64程式的.data段,就沒辦法隨著指令塊一起注入到目標程式中,而且這段程式碼中取"it's me
~"地址的技巧,對於彙編初學者是很有意思的,程式碼不長,建議在大腦裡執行一遍,並找到其中的奧妙。
3. 怎麼實現注入?
本篇文章僅僅用於學習目的,注入的場景和方法都比較簡單,思路在文章第1節已經介紹過了,就是"暫停被注入程式 ->
替換即將執行的指令塊 ->
通知被注入程式繼續執行",由於有些場合還需要保證注入操作的隱蔽性,所以以下程式還在注入指令塊完整執行後,將被注入程式恢復到了注入前的狀態:
(理解這段程式碼,至少需要學習ptrace()系統呼叫的作用,如果有核心基礎,也可以更深入的學習一下ptrace()的內部原理,另外,CODE巨集對應的內容,即為利用文章第2節描述的方法,提取的機器碼)
// inject.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <errno.h>
// 注入指令塊(列印"it's me ~")
#ifdef ENV_I386
#define CODE \
"\xeb\x15\x5e\xb8\x04\x00\x00\x00" \
"\xbb\x02\x00\x00\x00\x89\xf1\xba" \
"\x0c\x00\x00\x00\xcd\x80\xcc\xe8" \
"\xe6\xff\xff\xff\x69\x74\x27\x73" \
"\x20\x6d\x65\x20\x7e\x0a\x00"
#define REG_IP regs.eip
#else // X_64
#define CODE \
"\xeb\x19\x5e\x48\xc7\xc0\x01\x00" \
"\x00\x00\x48\xc7\xc7\x02\x00\x00" \
"\x00\x48\xc7\xc2\x0c\x00\x00\x00" \
"\x0f\x05\xcc\xe8\xe2\xff\xff\xff" \
"\x69\x74\x27\x73\x20\x6d\x65\x20" \
"\x7e\x0a\x00"
#define REG_IP regs.rip
#endif
#define CODE_SIZE (sizeof(CODE)-1)
/* 往pid程式的addr地址處寫資料 */
void putdata(pid_t pid, unsigned long addr, void *vptr, int len)
{
int count = 0;
long word;
while (count < len)
{
memcpy(&word, vptr+count, sizeof(word));
word = ptrace(PTRACE_POKEDATA, pid, addr+count, word);
count += sizeof(word);
if (errno != 0)
printf("putdata failed: %p\n", (void *)(addr+count));
}
}
/* 讀pid程式addr地址處的資料 */
void getdata(pid_t pid, unsigned long addr, void *vptr, int len)
{
int i = 0, count = 0;
long word;
unsigned long *ptr = (unsigned long*)vptr;
while (count < len)
{
word = ptrace(PTRACE_PEEKDATA, pid, addr+count, NULL);
count += sizeof(word);
ptr[i++] = word;
if (errno != 0)
printf("getdata failed: %p\n", (void *)(addr+count));
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct user_regs_struct regs;
char backup[CODE_SIZE+1];
if (argc != 2) {
printf("Usage: %s {pid}\n", argv[0]);
return -1;
}
// 通過啟動引數,獲取被注入程式號,並attach
pid = atoi(argv[1]);
ptrace(PTRACE_ATTACH, pid, NULL, NULL); // SIGTRAP
if (errno != 0)
printf("attach failed: %s\n", strerror(errno));
wait(NULL); // 收到回覆訊號,保證後續過程在attach完成的情況下執行
// 獲取被注入程式當前暫存器值
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
// 備份*ip暫存器指向的指令塊
getdata(pid, REG_IP, backup, CODE_SIZE);
// 替換為CODE指令塊
putdata(pid, REG_IP, CODE, CODE_SIZE);
// 恢復被注入程式的執行
ptrace(PTRACE_CONT, pid, NULL, NULL); // SIGCONT
// 注入程式最後一條指令為int3,此為即為等待注入指令塊執行完畢
wait(NULL);
// 等待使用者輸入,方便檢視結果
printf("continue to execute the orginal process, press any key ..\n");
getchar();
// 將修改的指令塊恢復為備份內容
putdata(pid, REG_IP, backup, CODE_SIZE);
// 恢復被注入程式暫存器值
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
// detach,被注入程式恢復正常執行
ptrace(PTRACE_DETACH, pid, NULL, NULL);
return 0;
}
4. 視訊演示
a. 執行gcc inject.c -oinject -g -Wall,編譯生成注入程式
b. 檢視who程式號N,並執行sudo inject N,進行動態注入操作
c. 觀察who程式執行視窗是否列印了"it's me ~"
d. 回到inject程式執行視窗,按任意鍵結束inject程式,並恢復who程式到注入前的狀態,繼續不停的列印"who are you ?"
視訊連結:https://v.youku.com/v_show/id_XMzgzMTM1NTU1Ng==.html?spm=a2h0k.11417342.soresults.dposter
5. 參考
ptrace實現動態注入:http://www.cnblogs.com/r1ng0/p/9585356.html
gdb的工作原理:https://blog.csdn.net/u012658346/article/details/51159971
原文作者:xinpoo
原文連結:https://bbs.pediy.com/thread-246948.htm
轉載請註明,轉自看雪論壇
看雪閱讀推薦:
1、[原創]小白文——好玩不貴的無線遙控器克隆指南
2、[翻譯]核心模式Rootkits:檔案刪除保護
3、[翻譯]國外2018最新區塊鏈教程英文版,大膽翻譯,助力論壇『區塊鏈安全』開設第四棒!
4、[原創]看雪安全峰會—《從WPA2四次握手看KRACK金鑰重灌攻擊》
5、[原創]2018看雪CTF第十五題WP