DEF分析与打造其脱壳软件
初来贵地.献析文一篇. 请高手勿笑 .
我不知道有这么个地方, 以后请大家多指教.
我写的不怎么好的地方,请大家指正.
我不怎么懂脱壳,请不要大家找我脱壳。。。
作者:牛博威
Email:advice107@sina.com
http://nboy.cnwlt.com/
qq: 343538175
加壳软件一种可以对别的程序进行修改但是不影响其使用的工具软件。程序员经常利用加壳软件对自己的软件进行加壳,以加密或者压缩自己的软件。加壳后的软件一般来说难以被逆向分析,因此,脱壳软件应运而生。从某种角度来说,了解加壳和脱壳技术的原理是很有必要的。
下面我以Def为例说明其加壳原理,同时,我们改造Def使其具有针对自身的脱壳功能。
Def是一款源码开放的小型加壳软件,精炼易懂,便于学习。其源代码和程序可以去http://protools.cjb.net下载。
首先根据其源代码分析其加壳原理。它的加壳流程主要如下:
1、 以文件内存映象方式打开被加壳文件,并判断是否是有效的PE文件且不是Dll文件;
2、 遍历所有节表,根据节表名判断是否是输入表,资源节表或者其他节表,如果不是,定位到该节区,根据本节区大小对该节区的内容进行简单的异或加密,加密方式如下:
_encrypt: ;加密该节区
xor byte ptr[esi],al ;进行异或加密
inc esi ;esi指向节区下一个字节
dec eax ;判断是否到节区尾部,同时也是下一次异或操作的变量
jne _encrypt
然后把节表名称修改为.def,对未进行加密的节表,添加未加密标志,具体见下文;
3、 为被加壳文件添加自解密部分,用于被加壳文件运行时候自行解密。这一段可以参考源代码中的_loader部分。这段自解密代码被添加到文件节表的后面,同时修改文件的入口地址为此处的偏移地址。添加自解密部分的代码如下:
mov esi,offset _loader ; 初始化_loader的偏移地址
mov ecx,_loader_size ; 初始化loader部分的大小
rep movsb ; 把loader部分拷贝到被加壳文件的节表尾部
;其实这里最好判断一下是否有足够空间放置本段代码
4、 最后关闭文件内存映象;
根据上面的流程分析,再仔细研究一下其代码,相信你可以很快明白它的具体工作方式。下面我们来修改def,使其具有脱壳功能。
Def手工脱壳很简单,我以脱去Def自身的壳为例,说明其脱壳流程:
1、 用Trw载入Def;
2、 F10单步运行至xxxx:400244 push dword ptr 00401000 处,其中00401000为Def原来的入口地址。下命令Suspend,将进程挂起,F5回到windows界面;
3、 打开Peditor,点task,选择被挂起的Def进程,点右键dump(full),保存为unDef;
4、 用Peditor修改unDef文件的入口地址为00001000,注意这里是00401000减去00400000得到的。
好了,这样unDef就是Def的脱壳文件。我们现在要修改Def为自身的脱壳程序,也就是要对unDef进行处理。
首先,脱壳程序应该可以判断一个文件是否被Def加壳,利用Def的判断方式,判断第一个节表名称是否为.def便可以。故此,修改:
:00401091 813A2E646566 cmp dword ptr [edx], 6665642E ;.def ?
:00401097 0F858B000000 je 00401128
为:
:00401091 813A2E646566 cmp dword ptr [edx], 6665642E
:00401097 0F848B000000 jne 00401128 ;如果第一个节表名不为.def则跳出
;请注意这里最好不要改成jmp
现在就可以对加壳文件进行脱壳了,由于def采用了简单的异或加密,因此解密部分不需要修改。找个加壳文件实验一下,竟然发生异常。为什么这样呢?因为Def并不是把所有的节表都加密,对于引入表,资源节表等,def是不处理的。但我们的unDef会把所有的节表都进行解密,因此当然会发生错误。也许你会问,在程序中不是有call _is_encryptable(也就是:004010A7 call 004011A0)用来判断是否应该被加密么?不错,虽然如此,但是仔细再看一下_is_encryptable这个函数,它通过节表名来判断节表的修改合法性,而被加壳的文件所有节表名称都被改成了.def。因此,对这个函数来说加壳文件中所有的节表都应该被修改。
难道我们就没有办法判断了么?当然不是,仔细研究一下加壳文件的引导部分,也就是Def源代码中的_loader部分,在程序自身解密的时候有cmp byte ptr [esi+07], 00,其中esi指向节表头,如果[edx+07]=0 则跳过解密部分,否则进行解密。这样一来,我们修改unDef的解密部分。从VA=004010A7开始,修改后如下:
:004010A7 807A0700 cmp byte ptr [edx+07], 00 ;判断节区修改标志
:004010AB 90 nop ;保证文件后面的内容不变
:004010AC 90 nop
:004010AD 90 nop
:004010AE 740F je 004010BF ;如果不该解密,便跳过解密部分
到这里,就可以把所有加密部分恢复。但是脱壳文件仍然无法运行,因为我们还没有把文件的入口地址修改过来。看一下def如何修改入口地址,你可以很容易编写恢复入口地址的代码,如下:
:004010D6 50 push eax
:004010D7 8B4225 mov eax, dword ptr [edx+25] ;edx指向节表最后
;[edx+25]为真正的入口地址
:004010DA 2D00004000 sub eax, 00400000 ;入口地址的RVA->eax
;减去imagebase,我这里图省事,直接减去00400000H,正规的应该从文件中读取
:004010DF 894728 mov dword ptr [edi+28], eax ;edi+28指向入口地址
:004010E2 58 pop eax
:004010E3 E92B000000 jmp 00401113 ;跳转到添加_loader处
:004010E8 90 nop ;保证原来的文件内容不变
最后,修改_loader处的代码为自己的信息(这就是上面为何跳到401113)。把 "The Undef Made by NBW QQ:37122085 http://nboy.cnwlt.com"的ASCII码覆盖到文件地址1154处,同时要修改:
:00401118 B92A000000 mov ecx, 0000002A
因为这个地方定义了添加的新代码的长度,如下:
:00401118 B93A000000 mov ecx, 00000036 ;36H就是我添加的信息的长度
到此,我们的unDef就制作完毕了,找一个被Def加壳的程序,用unDef进行脱壳,OK!很好用。
最后再多说几句,上面的这些地址除注明外都是程序的虚拟地址,在修改unDef的时候,不妨用Hiew打开,按F3,再按F2进入Asm修改状态,你可以直接输入汇编程序进行修改,但是由于Hiew支持的Asm代码不全面,因此有些语句还需要在16进制状态进行修改。从这个修改过程也可以看到,由于加壳软件对原文件的破坏,我们做的脱壳软件并不可能100%地恢复加壳软件原状,至少节表的名称和属性没有修改过来。因此,一些软件被专家级别的加壳软件所加壳,在脱壳的时候出现较大偏差是很正常的。
下面是源代码分析篇:::::
Def是一款典型的加壳软件,软件可以进行简单的异或加密,并且也未对输入表等进行处理,总体来说非常简单。
我在这里对其源代码写了详细的注释,仔细看一看会有不少收获。源代码也有注释,但是我现在还不明白那是哪国天书。要有高手明白,不妨告知一二,这里先谢过了。unDef是我写对Def进行修改,使其可以脱自身的壳。
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
; DEF - prostacki encryptor plikow PE EXE
; kompatybilny z 95,NT
;
;原作: bart/xtreeme
;
;这是一款典型的加壳软件,软件可以进行简单的异或加密,并且也未对输入表等进行处理,总体来说非常简单。
;其源代码是开放的,并且非常简练和易读,仔细看一看会有不少收获。源代码也有注释,但是我现在还不明白那是那国天书,
;要有高手明白,不妨告知一二,这里先谢过了。
;
;
;
;注释:牛博威
;http://nboy.cnwlt.com
;QQ : 37122085
;Email: [email]advice107@sina.com[/email]
;我的资料被人拿走了,因此有些地方应该还有问题,不足指出还请大家包涵和指出
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
.586 ; 定义用的指令结合
.mmx ; +mmx
.model flat,stdcall
includelib e:\dev\masm\lib\kernel32.lib ; 包含所需类库
includelib e:\dev\masm\lib\user32.lib
includelib e:\dev\masm\lib\comdlg32.lib
include e:\dev\masm\include\kernel32.inc
include e:\dev\masm\include\user32.inc
include e:\dev\masm\include\comdlg32.inc
include e:\dev\masm\include\windows.inc
include pe.inc ;pe.inc中定义了所需的常量,多与PE格式有关
.data?
lpFilename db 256 dup(?) ; 文件名
lpFile MEMF <> ; 文件句柄
lpPeheader dd ? ; PE头
lpSectionTable dd ? ;指向第一个节表的头部
.data
lpOfn dd lOfn ; 定义打开文件所需要的结构
dd 0 ; hWnd
dd 0 ; hInst
dd offset szFilter ; filtr
dd 0
dd 0
dd 0
dd offset lpFilename
dd 256
dd 0 ; lpstrFileTitle;
dd 0 ; nMaxFileTitle;
dd 0 ; lpstrInitialDir;
dd offset szCaption ; lpstrTitle;
dd OFN_HIDEREADONLY ; Flags;
dw 0 ; nFileOffset;
dw 0 ; nFileExtension;
dd 0 ; lpstrDefExt;
dd 0 ; lCustData;
dd 0 ; lpfnHook;
dd 0 ; lpTemplateName;
lOfn equ $-lpOfn
szCaption db 'DEF v1.0 by bart/xt - wybierz plik do zaszyfrowania',0
szFilter db 'Pliki EXE (*.exe)',0,'*.exe',0
db 'Wszystkie pliki (*.*)',0,'*.*',0,0
szError db 'Nieprawidlowy format pliku!',0
szDef db 'DEF v1.0 by bart/xt',0
.code
_start:
push offset lpOfn ; lpOfn偏移入栈(废话)
call GetOpenFileNameA ; 调用通用对话框打开文件
test eax,eax ; 判断出错否?
je _exit
push offset lpFilename ; 文件路径入栈
call _open_file
inc eax ; 这种判断方式也算一种技巧
je _exit
test ecx,ecx
je _exit
dec eax
mov ebx,offset lpFile ;ebx->文件句柄,也相当于文件头
assume ebx:ptr MEMF ;把ebx赋值为MEMF结构
mov [ebx].file_handle,eax ;保存句柄
mov [ebx].file_size,ecx ;保存大小
push eax
push ecx
call _map_file ;以文件内存映像的方式打开文件,这样个方式的好处真是多多
test eax,eax
je _exit_close
test ecx,ecx
je _exit_close
mov [ebx].mem_ptr,eax
mov [ebx].mem_handle,ecx
;下面是文件合法性判断
cmp word ptr[eax],'ZM' ;判断文件头部的Dos标志“MZ”
jne _bad_format
add eax,dword ptr[eax+3Ch] ; eax->文件NTHeader
mov lpPeheader,eax
xchg eax,edi ;edi->文件NTHeader
cmp dword ptr[edi],00004550h ; 判断是否是PE文件
jne _bad_format
cmp dword ptr[edi+entrypointRVA],0 ;判断文件的入口地址为0否?
je _bad_format
test word ptr[edi+DllFlags],2000h ; 判断是否是Dll文件
jne _bad_format
movzx edx,word ptr[edi+NtHeaderSize] ;edx->NtHeader尾部,我记不清楚这里是否是节表开头了,你可以查一下
lea edx,[edi+edx+18h] ; edx->节表头部
cmp dword ptr[edx],'fed.' ; 判断该文件是否被Def加壳
je _bad_format
mov lpSectionTable,edx ; 保存节表头部
movzx ecx,word ptr[edi+numObj] ; ecx->节表数目
; edx <-- 节表头部
; edi <-- PE头
; ecx <-- 节表数目
_encrypt_sections:
; 判断edx指向的节区是否该加密
call _is_encryptable
test eax,eax
je _encrypt_skip
mov esi,dword ptr[edx+objpoff] ;esi->节区文件偏移地址
add esi,[ebx].mem_ptr
;esi->节区虚拟地址
mov eax,dword ptr[edx+objpsize] ;eax->节区大小
_encrypt: ;加密该节区
xor byte ptr[esi],al ;进行异或加密
inc esi ;指向节区下一个字节
dec eax ;判断是否到节区尾部,同时也是下一次异或操作的一个变量
jne _encrypt
inc eax
_encrypt_skip:
mov dword ptr[edx],'fed.' ;修改节表名称
bswap eax ;eax由01000000改为00000001,如果开始为0,则仍然为0
mov dword ptr[edx+4],eax ;把eax写入节表名称后面,这样做是为了标志该节表是否被加密
;1->被加密
;0->未被加密
mov dword ptr[edx+objflags],0C00000E0h ;修改节区属性为可写可读
add edx,objlen ;edx->下一个节表
loop _encrypt_sections ;转入下一个节表的处理
; korygowanie offsetow loadera
mov edx,dword ptr[edi+imagebase] ;读入基址
mov dword ptr[_ldr_imagebase],edx ;保存基址,便于被加壳文件的载入
mov eax,dword ptr[edi+entrypointRVA];读入入口地址
add eax,edx ;入口地址的VA
mov dword ptr[_ldr_host],eax ;保存入口地址,便于被加壳文件的载入
mov eax,lpSectionTable
sub eax,[ebx].mem_ptr
add eax,edx ; VA
mov dword ptr[_ldr_sections],eax ;保存节表头的地址
movzx ecx,word ptr[edi+numObj] ;节表数目
mov byte ptr[_ldr_count],cl
imul ecx,objlen ;ecx->节表大小
mov eax,lpSectionTable ;
sub eax,[ebx].mem_ptr
add eax,ecx ;eax->节表尾部的RVA
mov dword ptr[edi+entrypointRVA],eax;再次保存入口地址,上面的保存其实多余
add eax,[ebx].mem_ptr
xchg eax,edi
mov esi,offset _loader ; 初始化_loader的偏移地址
mov ecx,_loader_size ; 初始化loader部分的大小
rep movsb ; 把loader部分拷贝到被加壳文件的节表尾部
;其实这里最好判断一下是否有足够空间放置本段代码
push -1
call MessageBeep ;响铃
jmp _exit_unmap
_bad_format:
push 10h
push offset szDef
push offset szError
push 0
call MessageBoxA
_exit_unmap:
push [ebx].mem_ptr
push [ebx].mem_handle
call _unmap
_exit_close:
push [ebx].file_handle
call CloseHandle
_exit:
push -1
call ExitProcess ; bye bye
; 下面这一段被放入被加壳文件的节表尾部,作为程序运行时候的解密部分
_loader:
mov esi,987654321 ;这里的987654321只是为了给_ldr_sections留够4个字节的空间
_ldr_sections equ dword ptr $-4 ;定义_ldr_sections及其地址,请注意查找前面的设置
push 0 ;节表数目
_ldr_count equ byte ptr $-1
pop ecx ;ecx->节表数目,在前面有设置
_ldr_next: ;解密开始
cmp byte ptr[esi+7],0 ;判断这个节区是否该解密,根据[esi+7]判断,请参考前面的注释
je _ldr_not_encrypted ;如果不该解密,越过下面的部分
mov eax,dword ptr[esi+objrva] ;eax->节区的Rva
add eax,987654321 ;eax->节区的VA
_ldr_imagebase equ dword ptr $-4 ;请注意参考前面的设置
mov edx,dword ptr[esi+objpsize] ;节区大小
_ldr_decrypt: ;异或解密
xor byte ptr[eax],dl
inc eax ;下一个字节
dec edx
jne _ldr_decrypt
_ldr_not_encrypted:
add esi,objlen ;esi->下一个节区
loop _ldr_next ;开始下一个节区的解密
push 987654321 ;程序原来的入口地址入栈,这样,下面的ret便返回到_ldr_host处执行
_ldr_host equ dword ptr $-4
ret
_loader_end:
_loader_size equ $-_loader
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
;
; mov edx,offset section_header
; call _is_rsrc
;
; sprawdza, czy sekcja zawiera zasoby
;
; na wyjsciu:
; eax - 1 sekcja zasobow
; 0 brak zasobow
;
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
_is_rsrc proc near ;判断节表名称是否为rsr或者reso,因为这些节区不可以加密
cmp dword ptr[edx],'rsr.' ; rsrc
je _rsrc_section
cmp dword ptr[edx],'oser' ; resource
je _rsrc_section
mov eax,dword ptr[edx+objrva]
cmp eax,dword ptr[ebx+resource]
je _rsrc_section
sub eax,eax
ret
_rsrc_section:
sub eax,eax
inc eax
ret
_is_rsrc endp
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
;
; mov edx,offset section_header
; call _is_encryptable
;
; eax - 0 不可加密
; 1 可加密
;
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
_is_encryptable proc near
push ebx
mov ebx,dword ptr[lpPeheader]
call _is_rsrc
dec eax
je _bad_section
cmp dword ptr[edx],'ler.' ; reloc
je _bad_section
cmp dword ptr[edx],'ade.' ; edata
je _bad_section
cmp dword ptr[edx],'ete.' ; etext
je _bad_section
cmp dword ptr[edx],'adr.' ; rdata
je _bad_section
cmp dword ptr[edx],'slt.' ; tls
je _bad_section
cmp dword ptr[edx],'adi.' ; idata
je _bad_section
sub eax,eax
cmp dword ptr[edx+objpoff],eax ;判断节区的属性是否合法,下面的几个也是
je _bad_section
cmp dword ptr[edx+objpsize],eax
je _bad_section
mov eax,dword ptr[edx+objrva] ;保存rva
cmp eax,dword ptr[ebx+resource] ;判断节区的rva是否和资源的rva一样,如果一样则不可加密
je _bad_section
cmp eax,dword ptr[ebx+edatadir] ;我也不太清楚,你可以跟踪看一下
je _bad_section
cmp eax,dword ptr[ebx+import]
je _bad_section
cmp eax,dword ptr[ebx+reloc] ;reloc是说重定位表
je _bad_section
cmp eax,dword ptr[ebx+tls] ;我也不知道
je _bad_section ;
sub eax,eax
jmp _check_exit
_bad_section:
or eax,-1
_check_exit:
inc eax
pop ebx
ret
_is_encryptable endp
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
;下面是文件的打开和关闭部分,我就不献丑写注释了。大家要不太清楚,查以下书好了
; push offset lpFilename
; call _open_file
;
; Otwiera plik do zapisu i odczytu z podanej lokalizacji
;
; na wejsciu:
; lpFilename - nazwa pliku do otworzenia
;
; na wyjsciu:
; eax - uchwyt pliku lub -1 jesli blad
; ecx - rozmiar pliku
;
; modyfikowane rejestry:
; eax,ecx,edx
;
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
_open_file proc near
pop eax
pop edx
push eax
push edx
push FILE_ATTRIBUTE_ARCHIVE ; atrybuty
push edx ; nazwa pliku
call SetFileAttributesA ; usun atrybuty blokujace dostep do zapisu
pop edx ; pop nazwa pliku
sub eax,eax
push eax
push FILE_ATTRIBUTE_NORMAL
push OPEN_EXISTING ; akcja, otworz plik
push eax
push eax
push GENERIC_READ + GENERIC_WRITE ; tryb dostepu
push edx ; nazwa pliku
call CreateFileA
cmp eax,-1 ; sprawdz wartosc zwrocona przez CreateFileA
je _open_err ; jesli to -1 to znaczy, ze wystapil blad
; wyjdz z takim wynikiem z funkcji
push eax ; zapamietaj na stosie uchwyt pliku
push 0 ; 0 dla plikow mniejszych niz 4gb
push eax ; uchwyt pliku
call GetFileSize ; pobierz rozmiar pliku
pop ecx ; pop, uchwyt pliku do ecx
xchg eax,ecx ; zamien wartosci w rejestrach, tak, ze
; w eax znajdzie sie uchyt pliku a w ecx
; rozmiar pliku
_open_err:
ret ; wyjscie z funkcji
_open_file endp
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
;
; push hFile
; push cbSize
; call _map_file
;
; Mapuje plik do pamieci, wszystkie operacje dokonywane na pamieci maja fizyczne
; odzwierciedlenie na dysku
;
; na wejsciu:
; hFile - uchwyt pliku otwartego w trybie do odczytu i zapisu
; cbSize - rozmiar pliku
;
; na wyjsciu:
; eax - wskaznik do mapowanego pliku lub 0 jesli blad
; ecx - uchwyt mapowanego pliku
;
; modyfikowane rejestry:
; eax,ecx,edx
;
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
_map_file proc near
pop eax ; adres powrotu
pop ecx ; rozmiar pliku
pop edx ; uchwyt pliku
push eax
sub eax,eax
push eax
push ecx ; rozmiar pliku(+opcjonalnie rozmiar
; dodtkowego obszaru pamieci)
push eax ;
push PAGE_READWRITE ; tryb dostepu do mapy pliku
push eax
push edx ; uchwyt pliku
call CreateFileMappingA
push eax ; zapamietaj uchwyt mapy pliku
sub edx,edx ; zeruj edx
push edx ; ilosc bajtow do mapowania, gdy 0
; mapowany jest caly plik
push edx ;
push edx ;
push FILE_MAP_WRITE ; flagi dostepu
push eax ;
call MapViewOfFile ; w eax wskaznik do mapy pliku
pop ecx ; uchwyt mapy pliku do ecx
ret
_map_file endp
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
;
; push lpMap
; push hMap
; call _unmap
;
; zamyka mapowany plik + zapisuje zmiany jakie dokonano na mapie pliku
;
; na wejsciu:
; lpMap - wskaznik do mapy pliku
; hMap - uchwyt mapy pliku
;
; na wyjsciu:
; brak
;
; modyfikowane rejestry:
; eax,ecx,edx
;
;哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪哪
_unmap proc near
pop eax
pop ecx ; hMap
pop edx ; lpMap
push eax
push ecx ; zapamietaj parametr dla CloseHandle
push edx ; lpMap
call UnmapViewOfFile ; usuwa obraz mapy pliku z przestrzeni adresowej
; naszego procesu
call CloseHandle ; zamknij mape pliku
ret
_unmap endp
end _start
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [討論]著作權及智慧財產權問題 2975
- [讨论]我的零碎想法, 大家将就看吧 2823