[原创]简略翻译《Write Small Shellcode》——NGS
发表于:
2007-12-28 12:32
15032
[原创]简略翻译《Write Small Shellcode》——NGS
【文章标题】: 简略翻译《Write Small Shellcode》——NGS
【文章作者】: fqucuo
【作者邮箱】: fqucuo@163.com
【作者QQ号】: 389990968
【下载地址】: 自己搜索下载
--------------------------------------------------------------------------------
【详细过程】
简略翻译《Write Small Shellcode》——NGS
声明:此文是我在看阅failwest Sir's 《深入浅出MS06-040(看雪网络版).pdf》提到的,搜了一下有就下了,不过是英文版的,至于有没有人翻译我不知道,因为自己也是初学者,准备比赛需要很多时间,也无暇去费心去找有没有翻译过的,反正自己先看了再说。强烈建议先看“完全分析failwert Sir's Shellcode”一贴,绝对不是为了顶贴和炫耀,因为文中中间部分与我的原帖(其实是failwest Sir的shellcode)有很多类似之处,文中我会提到,因为不想占用过多篇幅,使之又臭又长(原文挺长的,应该是我英文太烂),所以省去,届时将会出现请参阅XXX贴之类的,所以如果有需要就在看雪找。
由于本人英文水平实在有限,如果英文水平好的就看英文版的,我尽量用通俗的语句解释,再加上自己也是新手,所以会保持原创的所有精华之处,当然,一些见解和心得我也会说出来,也许就是自己写兴奋了连英文都没看就呼啦出来了(这也是我建议读英文版的用意),或许这样对跟我一样的新手吸收会快一些。
正文:
这个bindshell的文档使用了191个非空字节代码, 大概的描述了如何写出短小精悍的shellcode
当我们在“攻击”软件的时候,shellcode的大小是至关重要的,因为我们经常会遇到仅有一小部分可利用的空间,
我现在假设阅读者已经具备了汇编语言的能力
介绍:
这个任务主要是完成以下三步:
1. Bind 一个shell到6666端口
2. 再建立一个链接
3. 安全的退出
这段代码必须运行在NT4, 2000, XP和2003,并且建立在以下代码上,并且假定eax寄存器指向我们的shellcode首地址(具体情况灵活发挥):
void main()
{
unsigned char sc[256] = "";
strncpy(sc, "Shellcode here", 256);
__asm
{
lea eax, sc
push eax
ret
}
}
接下来,我们再说一下细节:
这段shellcode里面不能有空值(0x00,因为会被strncpy截断)
这段shellocde必须运行在栈空间里
Winsock没有被初始化
我们假设eax指向我们的shellcode的首地址(具体细节部分请参阅failwest Sir's 教程)
所有的代码将会附加在后面,首先,我先介绍几个关于这个任务的注意事项
如何写出小巧的shellcode
1. 使用短指令
我们知道,x86指令是变长的,有些时候不同长度的指令却可以完成相同的两件事,现在我介绍几个非常有用的“单字节”的指令
xchg eax, reg 交换两寄存器值
lodsd/lodsb 从esi所指向的地址中读取dword/byte 到 eax中,并且esi会根据所传数据的字节数自加
stosd/stosb 与上面相反,是从eax读出dword/byte 到edi所指向的地方
pushad/popad 这个嘛,不多说了,地球人都知道
cdq 扩展32位寄存器到64,其高字节部分根据eax高位的符号位扩展,当eax == 0 或者 eax <0x80000000的时候,edx = 0,反之edx = -1(0xFFFFFFFF),这样的话我们可以在我们能控制的清空下实现edx清零的操作
2. 一条指令完成多件任务
例如上面的xchg 就相当于mov ebx, eax mov eax, reg, mov reg, ebx 等等此类指令不列举了
3. API法则
有些时候API需要一个确定的值或大小,不管怎样,通过实验,我们确定是可以解决的。比如,许多API使用了结构成员,其结构成员必须指定大小, 并且需要一块足够大的空间保存参数值,否则运行起来有可能会不正常, 但是,如果我们已经知道某些栈空间已经释放,并且足够的大小,我们就可以利用API的容忍度(这点翻译的挺绕口)去设置结构体参数
许多API使用非空值最为参数,但是他们通常都是在最后入栈的,相比之下,我们传入空寄存器倒不如清空一块栈空间来的容易,之后我们就仅需要传入非空值即可,当我们连续的调用几个需要此类大小空间的作为参数的API的时候,这个时候,这种方案将是十分有意义的!
对于我们来说,就可以在栈空间中为API划出一块足够大的空间作为API的结构体参数,通常,我们可以使用单字节指令“push esp”作为结构体指针参数传入,某些情况,API不做边界检查的。
4. 不要认为自己只是个程序员
未翻
5.如何有效的使用寄存器
x86寄存器不全是等价的,有些有用的指令只能使用在指定寄存器上,确定使用某几个寄存器是经常的也是必须的, (比如用来保存API函数地址的ebp, edi, esi 等,lodsb stosd等都可以有效的控制他们的位置)使用这些寄存器来保存信息远比用栈来保存信息有意义的多(对我们来说)
6. 如何压缩我们的shellcode
这段不翻译了(又臭又长),具体就是说如何寻找一个高效的压缩代码的方法,文中使用的是hash,并且是压缩成一个字节,这些failwest Sir都提到过的,在我逆向failwest Sir的文中他用的是压成4字节的值,这样碰撞几率就很低了,也就有很多方式解决,当然这样这也是最稳最通用的,不过本文题目就是small,那就跟作者玩狠的吧!但是事先一定要确定你将使用的有哪些API字符串,并且通过写程序将所有的函数名都通过一个值hash,直到确定一个有效值就OK了
为了使我们的shellcode能够运行在各个版本中,我们要先完成两件比较重要的事情:
1. 确定需要哪些函数
2. 使用这些函数完成哪些任务
这些函数必须能够完成我们下面的任务
ws2_32.dll
WSASTartup 因为Winsock没有初始化,所以我们需要他
WSASocketA 创建套接字
bind ---
listen ----
accept ----- 这些就不想解释了,不懂了回去翻书
kernel32.dll
LoadLibraryA 我们需要他去获得ws2_32.dll的模块句柄
CreateProcessA 创建一个子进程用以执行我们创建客户端链接的任务
ExitProcess 结束已完成使命的子进程
下面这块我也真懒得翻(主要是因为我对hash算法理解不是很深)不过大体意思有必要讲一下:
当我们要完成以上函数的调用时,必须在shellcode中放置这些函数名,但是实际情况允许你放入这么多的字符串吗?绝对是不行的,那么我们就要运用一套行之有效的方式取解决字符串的问题,文中使用hash也不是随便就找个差值随便就可以用的,首先我们确定要将这些字符串都被hash成为一个字节,(牛吧),其实道理很简单,我上面提到过了,但是一定要遵守几个原则,
1. 非空
2. 在茫茫多的函数中查找(查找方式自己写程序实现)
3. 这个值与寄存器搭配之后指令还要最短的
4.通过这个值保证能够准确无误的匹配到我们的函数上去
....
下面的这段文字又臭又长(哈哈,自己英文烂还要埋怨别人。。),所以就不翻了,其最终确定的一个算法是这样:
hash_loop:
lodsb
xor al, 0x71
sub dl, al
cmp al, 0x71
jne hash_loop
可以看到这个是通过异或,其值为0x71(注意:此值是与当前任务中的API异或并且在异或完所有kernel32中的API(甚至包括ws2_32中的)所得到的值,绝不是通用值,选择异或或者是移位一定要根据现实情况决定)
OK,这样我们就得到了我们的函数字符串值(单字节的哦)
0x59 ;LoadLibraryA 相当于pop ecx
0x81 ;CreateProcessA 相当于 or ecx, 0x203062d3
0xc9 ;ExitProcess
0xd3 ;WSAStartup
0x62 ;WSASocketA
0x30 ;bind
0x20 ;listen
0x41 ;inc ecx 相当于inc ecx
0x43 ;C 等价于inc ebx
0x4d ;M 等价于 dec ebp
0x64 ;D 等价于 FS:
动态查找API这块我不讲了吧?看过“完全分析failwert Sir's Shellcode”一贴的人绝对是不需要这部分的,好,我们直接跳到下面建立连接部分:
还是要接着原帖讲(天知道还有这篇文章,我也是在发完贴后才发现的,还好能串起来)
原文最后部分是找到了LoadLibraryA,只不过作为例子failwest Sir只是实现了MessageBoxA,那么下面我们就是玩真的了
完成我们的bindshell:
这里开始我将会以逆向的方式讲解,原文的精华之处如果被我发现了我绝对不加保留的奉献,当然,漏过的。。。
现在的寄存器状态:ebp 保存了ws2_32的基地址, edi指向第一个Socket需要的函数
记得例子push的是user32吧?换成ws2_32就行了(这点有点跳跃,不过结合英文版附录中的代码看不会有任何问题的)
在我们使用这些函数之前,我们首先要初始化Socket(套接字), 需要去call WSAStartup, 而这些函数地址就存在我们edi所指向的地址中,我们先来看下WSAStartup的声明(查MSDN去)
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
我们将使用栈空间去构造WSADATA,因为他是个out值,我们不需要去初始化他,我们只需要确定他不会写到我们重要的代码就ok了,我们已经有了很多栈空间了(哪儿来的?回去看上一个贴去)所以我们可以放心大胆的用就是了
pop esi ;去WSAStartup
push esp ;给他一个大空间地址用来outWSADATA结构
push 0x02 ; 版本号一定要给的
lodsd
call eax ;读出来call就是了
如果函数返回成功将会返回0值,也就是eax = 0,那么我们就相当于获得了一个0值寄存器,这个时候我结合其他的几个套接字函数,他们需要大量的0值作为参数传递,那么我们何不妨选择一个大点的结构填充为0,到时候谁用谁拿去就是了呗,那我们选择STARTUPINFO这个结构的大小填充个全0的栈空间(STARTUPINFO够大了吧?),还可以给一个不用的地方放一个4字节的0,留着以后用呗,照着这个思路有下面代码:
mov byte ptr [esi + 0x13], al ;留给cmd字符串用的,用作0结尾
lea ecx, [eax + 0x30]; 取结构大小0x30
mov edi, esp
rep stosd ;填充全0
下面我们来看WSASocket,它需要6个参数
SOCKET WSASocket(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFO lpProtocolInfo,
GROUP g,
DWORD dwFlags
);
其中我们仅仅关心的是af和type这两个参数(看到了吧?往往有用的参数总是最后被压栈的),这两个参数我们需要分别放2和1
其他的统统给0。WSASocket将返回一个标识用来操作剩下的socket函数,所以我们准备用ebp保存他
inc eax ; 因为eax我们知道是0,Inc eax eax = 1(SOCK_STREAM)
push eax
inc eax ; eax = 2 (AF_INET)
push eax
lodsd
call eax ; WSASocket
xchg ebp, eax ; 保存SOCKET标识到ebp (这段也是很有技巧的)
咦?不是6个参数吗?怎么只传了两个就call了,其实配合上面的就理解了,_stdcall方式调用,内部函数不管你push了没push他总会去取栈顶上的值,并且帮忙恢复堆栈,也就是说我们在push有效值之前esp栈顶指针已经指向了一块全0的大空间,这时候仅仅需要传有效值到栈顶就行了(这点是很有技巧性的,值得学习)
下来就是监听了,在监听之前我们要先完成bind函数的调用
int bind(
SOCKET s,
const struct sockaddr FAR *name,
int namelen
);
回想一下我们的程序,我们还需完成那几步实现bind
1. 创建并初始化sockaddr_in结构
2. push 一个结构长度
3. push sockaddr结构的地址 改成sockaddr_in
4. push SOCKET标识 s
不管怎样,我们只需要稍微修改一下规则,就可以高效的完成这些任务。首先,sockaddr_in结构大多数可以为0,我们只需要关心的是他的
short sin_family
u_short sin_port
这两个参数,第一个为AF_INET(2),第2个是端口号,因为我们需要6666端口,其16进制位0x1A0A那么在空栈的基础上再压入栈就可以了,这里还要说一下,参数namelen不是很重要,给0即可,所以为了对齐上面的0x1A0A(32位压栈只能4字节4字节的压),又因为sin_family和sin_port都是short,我们只用压一个DWORD就够了,这个值确定为0x0A1AFF02,先看代码:
mov eax, 0x0A1AFF02
xor ah, ah ; FF清0,还记不记得我们的规定,不能有0值出现在shellcode中的
push eax ;这一步就相当于完成了赋值给结构体中sin_family和sin_port的过程
push esp ; 压入结构体地址
push ebp; 压入SOCKET表示
lodsd
call eax ;OK调用
心得:这点技巧真的不是盖的,所以要想成为专家,路确实挺长。。。
剩下的任务就是完成建立listen和accept, 呼快完了,别休息!可要知道哥们是在北京时间凌晨3:49分啊,今年第一次熬这么晚的,不说了继续!
int listen(
SOCKET s,
int backlog
);
SOCKET accept(
SOCKET s,
struct sockaddr FAR *addr,
int FAR *addrlen
);
到了这部分花样更多了,看看bind listen accept有什么相似之处?对了,就是最后压栈的总是SOCKET标识,首先这点插点花,看下我们的堆栈:
bind需要的栈:SOCKET s ;push ebp
const struct sockaddr FAR *name ; push esp
int namelen
listen需要的栈:SOCKET s ;push ebp
int backlog ; 这个参数作用不是很大,所以只要非0就行
accept需要的栈:SOCKET s ;push ebp
const struct sockaddr FAR *name ; push esp
聪明的朋友一定看到了规律,参数顺序基本相同(虽然用处不同),那我们就可以简化上面bind的代码,用一个loop去完成bind listen 和accept的任务!强啊!!!
call_loop:
push ebp
lodsd
call eax
test eax, eax
jz call_loop
至此我们已经成功的完成了在服务器上开端口设监听的操作,接下来就是我们扫尾工作了,我们首先建立一个客户端连接,去执行cmd.exe
哎~实在是太困了,剩下的这一小段大家可以参照英文版(不到20行了大哥!基本上都是代码),再说了,引用作者的话(in fact, wo could save a single byte of code by creating our shell without stderr , but let's be generous),人家都无所谓(gentleman)了,我们还赖着干嘛,行了,网络后门之类的事情本人没研究过,所以如何如何的也没啥见解,具体实战中,如何运用完全是基本功和经验技巧的问题,向高手学习总是没错的(可不是向我学习啊,我指的是作者和看雪的老大们!)
原英文版资料不发了,自己可以去网上搜到的
困了,睡去了。。。
--------------------------------------------------------------------------------
【经验总结】
不好意思,起晚了(谁叫睡的晚呢),一起来就来发帖(敬业吧),文章中用此达句是否恰当还望高人指点(千万别骂我啊。。水平问题啊,)其中一定有理解不深刻,甚至错误之处,还望过路高人万万指出,也给其他需要此贴的人以明示。
行了,废话不多说了,发帖先,有什么问题回帖讨论
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
2007年12月28日 12:20:54
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)