-
-
[原创]新人PWN入坑总结(二)
-
发表于: 2021-8-2 17:19 15532
-
接上文,但是我发现给的链接这位I春秋作者好像删除了,不知道因为什么。这里我说明一下,这个新人总结帖发的内容基本都是看那个给的链接教程之后,自己写的,有些exp,payload什么的可能复制了一点点,但基本都自己改过的。如果那位I春秋的作者看到了要求我删除一些他的东西,请直接私信。
1.题目给了libc库,需要查看一下版本,直接拖到Linux中运行一下./libc.so.6_x64,就可以知道是libc2.24的,但Linux中的libc没有该版本,所以用pwndocker来连接运行。具体怎么用看下方链接,同样docker也自行学习。
https://github.com/skysider/pwndocker
如果需要使用到这个libc调试,则在python中设置下列代码:
2.然后开始分析文件,常规checksec,开了NX,IDA打开文件找漏洞,发现输入4进入debug函数后可以泄露system的内存地址:
dlsym()的函数原型是
该函数在<dlfcn.h>文件中,handle是由dlopen打开动态链接库后返回的指针,symbol就是要求获取的函数的名称,函数返回值是void*,指向函数的地址,供调用使用。write函数的fd是1,所以就相当于直接打印在屏幕上,这里涉及linux系统调用号内容,可以直接查linux下的目录/usr/include/asm/中的unistd_32.h和unistd_64.h。
这段代码的意思就是把指向system函数的指针返回给v0,然后将v0格式化输出给fmsg,之后将fmsg原封不动打印在屏幕上,第一次看到猜不出来可以直接运行该文件试试呗。之后会进入一个debug_func(),这里存在栈溢出:
3.现在有了system的内存地址和栈溢出,就差/bin/sh字符串了。这里用IDA打开题目给的libc文件,可以找到bin/sh字符串的地址binsh_libc_addr和system的地址system_libc_addr。所以这就相当于有system的被libc加载的真实地址,那么system的真实system_addr减去system_libc_addr就可以得到Libc被加载进来的首地址libc_start_addr。即现掌握地址:libc_start_addr,system_addr,system_libc_addr,binsh_libc_addr通过计算可得:binsh_addr = system_addr - system_libc_addr + binsh_libc_addr。这不是栈溢出有了,system函数和binsh字符串的真实地址有了,这不直接getshell就完事了吗,闹啥呢,这破题目,没点技术含量。
4.但程序还是得走走,64位程序,所以需要使用ROPgadget表来查找pop rdi ; ret这个代码所在的地址,也是在Libc中查找到,然后加上libc_start_addr就可得到pop_rdi_addr。
5.之后计算偏移量,远程调试下进行,payload依次为padding + pop_rdi_addr + binsh_addr + system_addr。
6.再考虑输入情况:先在Linux下运行,所以能看到需要在接收到”: ”时可以输入4,然后进入到打印system_addr,打印完之后,需要从打印出来的system地址读取进我们设定的system_addr。
7.由于打印格式是@x0x7ffffff,所以在recvuntil”@x”,之后开始获取打印的system_addr:system_addr = int(io.recv(12), 16),以十六进制总共读取12位
8.读取完成system_addr后就可以开始输入payload,之后就可以interactive()。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,开了NX,打开IDA后分析找漏洞,可以看到main函数中begning函数中的,大多都是打印一些东西,然后由于输入到的是全局变量里,所以这些输入都没办法用来栈溢出。
2.接着往下深入,这里可以看到begning函数中有:
但是getchar()不能作为溢出,因为不管缓冲区多大,都只会从缓冲区读取一个字符。再深入进到first_day_corps()函数,然后可以看到函数change_major(),进入之后发现这里存在栈溢出:
3.由于NX保护,考虑用ROP,然后找找system和”bin/sh”,这里没有system,但是有int80,可以通过:
ROPGadget --binary pwn5 | grep "int 0x80"
这段代码来查找,int80同样可以用来开shell。但是int80开shell需要设置寄存器的值,所以需要用到gadget来查找对应的代码段:
ROPgadget --binary pwn5 | grep "pop eax ; pop ebx ; pop esi ; pop edi ; ret"
(另外,如果是64位程序则又会不一样了,需要具体查找一下资料。关于int80相关的系统调用,有个网址查询:https://syscalls.kernelgrok.com/ 这玩意需要挂VPN)
查到32位程序syscall调用int80所需要的条件:设置eax = 11 = 0xb, ebx = &(“/bin/sh”), ecx = edx = edi = 0. “/bin/sh”
▲这里怎么找都可以,只要最后条件满足即可。参考pwn_myself - else.py。代码执行过程中return直接就是返回跳转到当前esp的指向的内容。所以设置payload顺序也很重要。
原作者的代码是:
这里怎么ROP都可以,例如下列代码一样的效果:
4.考虑到程序中没有/bin/sh,而我们又会输入到全局变量中一些东西,所以可以将全局变量当作/bin/sh的地址,在输入过程中将/bin/sh输入到该全局变量中,可以通过IDA查出具体地址,因为全局变量的地址是不会改变的,方便之后调用。
▲挑选gadget时,注意不要选择有0a字样的,因为当我们往程序中输入时,回车即”\n”所对应的就是0a,如果有0a,则我们的输入不会完全输入进去,只会停在0a处,后面的就没办法再输入进去了。
▲由于int80的性质,远程调试时会直接错误,需要在IDA中反复进行错误丢弃才能进行shell操作,但是process本地运行却会自动忽略。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,只开了NX,然后IDA打开找漏洞。发现找不到什么漏洞,但是有个很奇怪的地方
查看反汇编代码后发现会有这么一串代码,v4是我们输入的东西,却被以函数形式调用。在汇编窗口中看下,发现call puts函数之后的代码形式是这样的。
也就是把我们输入的保存在var_8里的内容,给了rax,rax又给了rdx,之后call rdx。也就是我们输入的东西最后会被当初代码指令来执行。
2.程序不存在栈溢出,输入只能是4个字节,已经规定好了。%ld代表long int,四个字节,程序又没有一次getshell的后门函数,所以就只能靠这4个字节来getshell。
3.这里考虑使用one gadget RCE来一步getshell,首先在Linux下查找一下题目给的libc中有没有onegadget:
4.这样就可以通过一次跳转来getshell,但是第一条有限制条件,由于汇编代码中在call rdx之前有mov eax,0;即rax就等于0。(eax在64位程序下就是rax的低32位)或者先调试看看能不能满足条件,经过调试发现执行到call rdx时rax = 0,也满足要求,那么就尝试写payload。
5.本地中首先需要连接到指定的库文件中,可以先在linux中ldd libc库文件来看题目给的库文件是什么版本,之后修改这段代码让process能够连接到指定版本的libc文件。(利用pwndocker,或者自己下个对应版本的ubuntu—docker,然后安装python之类的)
io = process(['/glibc/2.24/64/lib/ld-linux-x86-64.so.2', './tvstation'], env={"LD_PRELOAD":"./libc.so.6_x64"})
6.由于不知道onegadget被libc加载进入之后是在什么地址,所以现在还需要泄露一个地址,刚好程序中有两个isoc99_scanf,第一个可以用来输入某个函数.got表中onegadget的地址,然后程序会打印出来该函数真实地址,对应代码为:
但注意输入的格式。由于输入格式为__isoc99_scanf("%ld", &v4)中的ld,也就是十进制有符号长整型,(l=long型,d=Decimal也就是十进制)所以我们需要将该地址转化为十进制数然后输入,因为scanf格式规定了,之后打印的格式是%016lx,其中x代表Hexadecimal,也就是16进制,16代表总共输出16位,0代表不足16位则高位补零。(如果不知道可以拿visualstudio测试一下就可以)
7.所以第一次输入应该为某个函数地址对应的十进制,这里选取setbuf函数,因为setbuf函数刚好在.got.plt表中,同时也从外部引用,在extern也有,十六进制地址为:0x600ae0(这里选用puts,printf,甚至__libc_start_main也行,只要满足在.got.plt表中和extern表中)也就是6294240,即io.sendline(“6294240”)。这样就可以打印出setbuf函数被加载进内存的地址,之后获取这个地址,先接收io.recvuntil("Value: "),使得接下来打印的是setbuf的内存地址,之后使用
setbuf_memory_addr = int(io.recv()[:18], 16)
表示总共接收18个字符,之后以16进制形式转化位int,10进制形式。这里总共应该会打印18个字符,16+0x,也就是18个。
8.之后计算偏移量,得到one_gadget_rce在内存中的地址即可:注意要转化为str字串形式发送
io.sendline(str(setbuf_memory_addr - (setbuf_addr_libc - one_gadget_rce_libc)))
9.最后io.interactive()即可getshell。
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec,开了NX保护,IDA打开找漏洞,发现程序特别奇怪,没有main函数,这里应该是把elf文件的符号信息给清除了。正常情况下编译出来的文件里面带有符号信息和调试信息,这些信息在调试的时候非常有用,但是当文件发布时,这些符号信息用处不大,并且会增大文件大小,这时可以清除掉可执行文件的符号信息和调试信息,文件尺寸可以大大减小。可以使用strip命令实现清除符号信息的目的。
2.虽然这里找不到main函数,但是start函数是一定会存在的,由于start按F5反汇编不成功,所以这里进入到start函数的汇编代码中:
由于start中的结构基本固定,最后基本上都是如下,所以这里sub_4011B1其实就是main函数,这里就可以点进去看了。
2.这里的main函数可以反汇编成功,那么就开始分析漏洞。第一个函数是sub_4374E0,进去之后如下
使用系统调用号37,也就是0x25,代表alarm。
3.sub_408800字符串单参数,且参数都被打印到屏幕上,可以猜测是puts。sub_437EA0调用sub_437EBD,并且fd参数位为0号,且接收三个参数,看下汇编代码:
调用0号syscall,推测为read函数。(系统调用号有)
4.进入sub_40108E函数中分析,这个函数处理了我们的输入,可以说就是关键函数了。看半天啥也没看懂,直接上调试。先输入十几个A看看,发现经过sub_400330函数之后,内存中输入的A,也就是a1处的内容被复制到了v2,这里先猜测是个类似strncpy函数的东西。然后看内容,既然局部变量v2只有0x40,而这个复制函数的的参数有80,也就是0x50,多了0x10。那么再调试看看,输入0x48个字节A,发现sub_40108E函数的ebp被我们改掉了:
sub_400330((__int64)&v2, a1, 80LL);
但是程序接着运行下去却不太行,陷入了循环,然后一直运行后崩溃,连之后的read(v8, (__int64)&v3, 40LL);这段read代码都没有运行。
5.再观察下程序,有个代码有意思:
在复制完字符串之后进入一个判断语句,如果开头是py,就直接retn,不经过下面代码,所以我们完全可以在这就直接返回。但是这里有个问题,这个return有没有汇编指令里的leave操作呢,如果没有,那rsp仍然在最前面,不会跳转到返回地址的地方,看汇编代码,可以看到最后是通过判断后跳转到了locret_40011AF,而这段地址里就是leave和retn的汇编操作,能够将rsp拉到返回地址处,那直接return就完事了。
6.那么这里就可以判断出来我们的输入会被复制到v2这个局部变量中,并且最多0x50,也就是说除开rbp,我们可以再控制一个该函数的返回地址。那么开始尝试呗。由于只有一个返回地址,没有后门程序,最先想到的肯定是onegadget,但是不知道libc版本,没办法onegadget。那只有一个返回地址可以做什么,那么只有栈劫持了。其它WP大多都是抬高rsp,我想可不可以降低rsp,通过一次payload来getshell,也就是通过ROPgadget搜索sub rsp,但是搜出来的都不太行,要么太大,超过0x50,要么就很奇怪。然后一般栈劫持需要一个ret来接着控制程序流,这里也没搜到。同时由于使用的复制函数经过调试就是strncpy,字符串里不能有\x00,否则会被当做字符串截断从而无法复制满0x50字节制造可控溢出,这就意味着任何地址(因为地址基本都会在最开始带上00)都不能被写在前0x48个字节中,彻底断了sub rsp的念想。所以还是抬高栈吧。但是抬高栈也有点问题,就是我们输入的被复制的只有0x50个字节,抬高有啥用,不可控啊。然后就想到之前的read函数,读了400个字节,而紧接着就是调用该函数。刚好局部变量v2第一个被压栈,与sub_40108E函数栈的栈底紧紧挨在一起,也就是说越过sub_40108E函数栈的栈底和返回地址就可以直接来到main函数栈。而main函数栈中又只有一个我们输入的局部变量v4,所以sub_40108E函数栈的返回地址之后的第一个地址就是我们输入的局部变量v4的地址。(这里通过调试也可以发现)
7.那么经过计算,其实只要有一个pop,ret操作,让rsp抬高一下就可以到达我们输入的首地址。但是由于经过前面分析,我们需要在程序开头输入py来使得该函数直接return,那么如果只是一个pop,ret操作,那么程序第一个执行的代码就是我们输入的开头,包含了py的开头,这就完全不可控了,开头如果是py那怎么计算才能是一个有效地址呢。
8.那么就只能查找add rsp,只要满足add rsp 0x50之上就可以完全操控了。这里至少需要0x50也是因为这是strncpy,不能将地址写到前0x48个字节,否则会截断,而最后返回地址的覆盖可以被完全复制是这里本来就是一个返回地址,保存的内容应该是00401216,也就是之前call sub_40108E的下一段地址。这里在复制的时候肯定被截断了,但是由于本来就是找到一个可用的地址,截断了覆盖的也只是将401216换成了add rsp 0x58;ret这个地址(如果我们的add rsp的有效地址地方包含了00,那指定会出错)。那么payload的语句应该是payload = "py" + "a"*(0x48-0x02) + add_rsp_addr + padding + 实际控制代码。
9.利用ROPgadget搜索add esp的相关内容,可以查到一个地址0x46f205,操作是add rsp, 0x58; ret,这样就可以顺利将栈抬升到0x58的地方,所以payload的组成应该是:payload = “py” + “a”*0x46 + p64(0x46f205) + “a”*8 + p64(addr2)+...(a*8是用来填充的,因为抬升到了0x58处,复制之后0x50处是一段空白地方,所以还需要填充一下使p64(addr2)能顺利被抬升至0x58处被执行)。后面的p64(addr2)和...就是我们的常规gadget操作了。
10.现在需要system函数和/bin/sh字符串了。没有Libc,system函数和/bin/sh也没有,所以这里需要输入/bin/sh字符串,然后system函数需要通过syscall来实现。(64位程序下是syscall函数,32位程序下就是Int 0x80)
11.这里先完成binsh的输入:
因为是64位程序,函数从左往右读取参数所取寄存器依次为:rdi,rsi,rdx, rcx, r8, r9, 栈传递,但是实际情况中是从右往左读取参数,也就是当只有三个参数时,读取顺序应该是rdx,rsi,rdi对应的为read(rdi,rsi,rdx)。
这里rdx是输入的大小,rsi是输入的内存地址buf(随便找一段可读可写的就行了),rdi是fd标志位,由于是通过syscall调用,所以除了配置三个read函数参数还需要配置系统调用号,也就是rax的传参为0x0。这里如果不使用syscall,其实也可以用我们之前猜出来的read函数的plt表,只是这样就可以不用设置rax了。
▲这里不能使用401202处的call read,因为call会压入下一行代码的作为read返回地址,那样就不可控了。这里选择系统调用是因为没有read在got表中的真实地址,不然其实调用got表地址也可以。
12.接着调用system函数,同样采用syscall系统调用,需要几个参数的设置rax=59,rdx=0,rsi=0,(这是调用syscall必须的前置条件,因为是linux规定的,可以上网查一下就知道)。都可以通过Pop gadget来实现,之后传参rdi为&buf,最后调用即可getshell。(59为系统调用号)所以紧接着的payload = p64(pop_rax) + p64(rax_value) + p64(pop_rdx) + p64(rdx_value) + p64(pop_rsi) + p64(rsi_value) + p64(pop_rdi) + p64(rdi_value) + p64(syscall)这里就必须的设置rax为0x3b了。
▲sh不能用来传给syscall开shell,但是int 0x80可以。syscall-64,int 0x80-32。
▲syscall是在上进入内核模式的默认方法x86-64。该指令在Intel处理器的 32位操作模式下不可用。sysenter是最常用于以32位操作模式调用系统调用的指令。它类似于syscall,但是使用起来有点困难,但这是内核的关注点。int 0x80 是调用系统调用的传统方式,应避免使用,是32位程序下的。
系统调用查询网址:https://syscalls.w3challs.com/
参考资料:
https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157
1.常规checksec一下,开了canary和NX,然后IDA打开分析漏洞。发现auth函数中可能存在栈溢出:
如果a1大于8h,而我们可以控制input,那么就可以造成栈溢出。再往上翻一下,发现就是将我们的输入通过一系列操作给到input,然后a1是input的长度。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
赞赏
- [原创]CVE-2021-22555_Netfilter堆溢出提权漏洞 22416
- [原创]Kernel从0开始(四) 28157
- [原创]Kernel从0开始(三) 29251
- [原创]Kernel从0开始(二) 32429
- [原创]Kernel从0开始(一) 44925