-
-
[原创]pwnablr.kr (刷题)之 Toddler's Bottle
-
发表于: 2018-3-23 20:37 6436
-
本来打算寒假的时候把这个题认真的刷一刷,结果...最近几天,把pwnable.kr
上前2部分的题刷了刷,写了写解题的过程。
根据提示,本题的关键在于文件描述符
ssh连接后,查看源文件:
根据维基百科上对于 file descriptor
的定义:
由 fd
的源码可知, 标准输入的内容为字符串LETMEWIN
,并且fd 的值为 0,所以第一个命令行参数应为0x1234 的 10进制 4660
, 通过管道符|
将 字符串输入程序,即可获得flag
echo 'LETMEWIN' | ./fd 4660
连接成功后,可以看到源码:
发现这个题的要求就是第一个命令行参数长度为20个字节,且经过函数check_password
处理后返回的值应该为 0x21DD09EC
,
所以在这里的话,我们选择 [0x21DD09EC/5]= 0x6c5cec8
+ 0x6c5cecc
来作为参数输入。
可以从网站上下载下来两个文件bof
和bof.c
,可以看到是一道简单的缓冲区溢出题:
用gdb
简单的调试下,可以发现实际开辟的栈空间大小为0x48
:
我们只需要传入"A*52" + "0xcafebabe" 即可获得shell:
但是需要注意的一点是,系统只是调用了system
函数,没有发现输入的话就会终止掉,所以我们可以向它传递一个标准输入:
(ps: 以前,我曾经一度认为函数func
传的参数就是0xcafebabe
,还在想这个题要干啥)
题目提示是一个加壳的文件,下载下来后看一下:
然后我们用UPX
工具脱壳
给文件添加运行权限chmod u+x flag
:
gdb
简单地 在main
处下个断点,发现运行到0x401184 <main+32>: mov rdx,QWORD PTR [rip+0x2c0ee5] # 0x6c2070 <flag>
这条指令时,将flag
的内容传入了RDX
寄存器:
got表覆盖技术
原理:由于got表是可写的,把其中的函数地址覆盖为我们shellcode地址,在程序进行调用这个函数时就会执行shellcode。由于本题中函数scanf
的 第二个参数没有加&
,传递的是值而不是地址。程序首先需要输入name
,然后输入passcode1
和passcode2
,相等的话会输出flag。
经过分析, name
和passcode1
间差了96个bytes,最后四个字节填充为got
表中printf
的地址(0x804a000
),这样就能在读取passcode1的时候,将system('/bin/sh')
的地址(0x80485e3
)写入got表中,得以执行。
这个题 比较简单,程序随机化时不指定seed 的话,生成的随机数都是一样的。本地测试下,随机数为1804289383
,将其与0xdeadbeef 异或,就可以得到正确的key = 3039230856
。
这个题考察了5种输入方式:
这题需要完成的就是实现这5步输入,然后就可以看到flag。
1.stage 1
编译运行一下,可以看到第一阶段已经完成:
2.stage 2
第二步是标准输入read(0, buf, 4)
,这个在第一题已经见过,但是由于\x00\x0a\x00\xff
无法通过命令行输入,stderr
也是这样,所以需要重定向输入,即fork创建子进程,pipe进行管道传输
可以参考这篇文章,理解管道传输,目前的代码为:
这一步用到了fork()
函数,作用是从父进程中打开一个子进程,这个函数会有2个返回值,一次是父进程,一次是子进程,而且 fork() 函数在子进程中返回 0,而在父进程中返回子进程的 PID,所以可以通过判断 fork() 函数返回值来分别对父/子进程进行操作。
3.stage 3
第三步是环境变量,char *envp[2] = {"\xde\xad\xbe\xef=\xca\xfe\xba\xbe", NULL};
4.stage 4
第四步是文件读写,需要在文件\x0a
中读取字符,判断是否为”\x00\x00\x00\x00″,所以把源码中的读操作改为写操作即可:
5.stage 5
第五步是socket 编程,和上一步类似,只需要把接收改为发送即可,但需要注意的是端口号port
是通过argv['C']
控制的,代码如下:
下面是完整的exp.c:
(ps:在网上也发现了python版本的实现,比较简洁,如http://www.akashtrehan.com/writeups/pwnablekr_todders_bottle/,但是我在测试的时候,发现第5步老是出问题...)
我们可以从网站上下载下来leg.c
和leg.asm
2个文件,简单地看下leg.c
的源码,可以知道这题的关键是找到key1()
,key2()
,key3()
三个函数的 返回值:
所以直接看3个函数的汇编代码:
key1()
以前做的都是x86汇编,没有做过arm相关的,所以简单地查了一下相关的信息:
PC代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:
1.取指(从存储器装载一条指令);
2.译码(识别将要被执行的指令);
3.执行(处理指令并将结果写回寄存器)。
而R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的指令或正在“译码”的指令。一般来说,人们习惯性约定将“正在执行的指令作为参考点”,称之为当前第一条指令,因此PC总是指向第三
条指令。当ARM状态时,每条指令为4字节长,所以PC始终指向该指令地址加8字节的地址,即:PC值=当前程序执行位置+8;
ARM指令是三级流水线,取指,译指,执行时同时执行的,现在PC指向的是正在取指的地址,那么cpu正在译指的指令地址是PC-4(假设在ARM状态下,一个指令占4个字节),cpu正在执行的指令地
址是PC-8,也就是说PC所指向的地址和现在所执行的指令地址相差8。
key()1 中,返回地址为r0, r0 = pc,所以r0 的地址为0x8cdc + 0x8
key2()
:
返回地址为r3,bx r6
跳转为thumb指令,r0为0x8d04 + 0x4 + 0x4
,即0x8d0c
。
key3()
:
r0
的值 为 lr
的 值,lx
寄存器 在函数调用时,将下一条指令地址传给lx寄存器,等待函数返回时调用,所以r0 = 0x8d80
三个返回值加起来 = 0x1a770
,也就是108400
。
看下题目源码,又看到了熟悉的read(fd,pw_buf,PW_LEN)
:
由于操作符的优先级问题,=
为14,而 <
为6
在下面这条语句中if(fd=open("/home/mistake/password",O_RDONLY,0400) < 0){
,fd
的值为0,
所以当执行到if(!(len=read(fd,pw_buf,PW_LEN) > 0))
时,实际上是从标准输入流读入了10个字节的内容:
`if(!(len=read(0,pw_buf,PW_LEN) > 0))``
这样,我们只需要先输入10个字符,再输入10个字符异或后,与前者相同。
我们可以考虑一种简单的情况,比如先输入10个0,再输入10个1。
根据百度百科介绍,Shellshock,又称Bashdoor,是在Unix中广泛使用的Bash shell中的一个安全漏洞。我们可以用export x="() { :;}; /bin/cat flag"
,添加以(){开头的环境变量,后面执行./shellshock
的时候,会以root
权限执行之前的命令。
nc pwnable.kr 9007
后,看到这个题是一个二分法的题目,找到假硬币,但是要在30s 内解100 次 才会print
flag,直接放上python
脚本:
另外,远程连接的话可能会超时,所以可以放到服务器的/tmp
文件夹下运行一下,即可。
这个题的第一印象就是源码太长了,让人很难看下去。。
百度了一下,才知道其实这个是一种扑克牌游戏。black jack是扑克牌游戏中的一种游戏规则,中文名为21点或者黑杰克。
检查了下代码中的输入,发现了betting
函数中存在漏洞:
用户输入bet
后,会判断是否大于500,却没有判断是否小于0。所以,我们输入-1000000
,再输掉的话,实际上就会赢得1000000
,这样就可以获得flag
了。
这个题意思也比较明显,就是说只要我们输入的字符中有一个和随机生成的lotto
数组中的字符相等的话,就会获得flag。所以,我们只需要输入6个一样的字符,直到获得flag为止:
可以看到,在cmd1
中,没有对/
字符进行过滤,因此我们可以传入绝对路径。只要想办法将/bin/cat flag
作为第一个命令行参数输入,绕过过滤的话,即可查看flag的内容,比如一种做法就是/bin/cat fla*
。
可以看到,在cmd2
中,对/
字符进行了过滤,所以我们要想办法构造出来/
字符,而pwd
命令可以打印出当前路径,如果我们切换到根目录下的话,不就能够利用/
字符了吗?
所以,我们切换到根目录下,执行下面的命令:
ps: 其实这个题的解法还是特别多的,比如下面这种:
根据题目提示,是UAF
漏洞,下载源码:
用64 位的IDA打开UAF
, 可以看到Woman
对象大小为24个字节:
在下面的rodata
数据段,可以看到虚表的地址:
而函数的调用就是这个虚表指针+偏移量,比如Human->give_shell
就是 vTable_ptr + 0
,所以只需要修改一个指针,我们只需要调用give_shell
函数,就可以得到shell
,后面函数需要调用introduce
函数,地址为vTable + 8
, 即 0x0000000000401568
。所以,我们先将这个地址写入文件中,然后作为文件输入:
然后 3->2->2->1 即可获取到shell
:
根据题目,我们可以下载memcpy.c
:
简单看下文件,两个字节复制函数slow_memcpy
和fast_memcpy
,前者是逐字节复制,后者是利用xmm
寄存器无cache复制,不足64个字节的话调用slow_memcpy
使用代码第一行所注释的编译选项gcc -o memcpy memcpy.c -m32 -lm
,编译文件:
运行程序,看看发生了什么:
使用gdb调试程序,发现执行到fash_memcpy
的时候,发生了异常:
fash_memcpy
函数用到了[movntps](https://baike.baidu.com/item/SSE%E6%8C%87%E4%BB%A4/8857860)
,参见百度百科解释,大部分涉及到128位内存变量操作的,内存变量首地址必须要对齐16字节,也就是内存地址低4位为0,否则会引起CPU异常,导致指令执行失败,此错误编译器不检查。
所以本题的关键就在于malloc 函数 所申请的字节大小,下图程序可以计算malloc程序实际申请的内存大小,只要size可以整除16,就满足本题的要求:
下面是示例代码:
根据目录下的文件readme
提示:我们只要连接到本地服务器的9026端口,执行asm 程序可以获取到flag文件,flag文件名称为this_is_pwnable.kr_flag_file_please_read_this_file.sorry_the_file_name_is_very_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo0000000000000000000000000ooooooooooooooooooooooo000000000000o0o0o0o0o0o0ong
程序首先申请一块内存,用0x90(nop)清零,再将字符串stub
的 内容复制到刚刚申请的内存空间中去,读取用户输入,执行程序。反汇编一下stub
,发现其功能是将全部的寄存器清零。
之后执行sandbox
函数(seccomp是一种安全运行模式),由于沙盒只允许运行open、read、write函数,所以我们可以使用x64的rax, rsp寄存器存放我们需要的内容,然后输出到标准输出流。我们可以使用pwntools
工具中的shellcraft
模块来编写我们的shellcode
,来读取/home/asm_pwn
目录下的flag
文件中的内容(关于shellcraft模块的使用,可以参考官方文档)。
完整的 exp
脚本如下:
to be ...
(其实,关于第一部分的wp,网上特别多,我写本篇文章的目的权当记录一下学习过程吧)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课