沿着控制流,看见算法。

拖动反汇编扫描镜。代码位置会同步点亮控制流节点与内存区域,把伪代码还原为可执行的推理。

展开完整推导
Decompiled / sub_1400010E00x1400010E0
1400010E0xorr9d, r9d
1400010E6movr8, rcx
1400010F2testrcx, rcx
1400010F5jzshort verify
140001102idivr8, 1Ah
140001109movzxeax, string_len26[rdx]
140001112mov[r10], al
140001119jneshort base26_loop
140001124xorcl, 07h
140001129callsub_1400011E0
140001134testrbx, rbx
140001138jneshort xor_loop
140001140callsub_140001220
140001220cmpbyte ptr [r9], target[rdx-1]
14000123Bcmpeax, 0Eh
140001241jesuccess
DRAG TO SCAN
Memory / control mapINPUT
INPUT
%lld
BASE 26
string_len26
LINKED LIST
node + next
VERIFY
14 bytes
INPUT 读取十进制数,准备按 26 进制拆分。
INPUT

第一次看逆向伪代码,先把陌生动作改写成熟悉结构。

入口是数字

main 分配链表头,读取一个 long long,然后进入 sub_1400010E0。题目不是直接输入 flag,而是验证一个数字经过转换后的结果。

scanf("%lld") → sub_1400010E0()

除法藏着取模

copy_input - 26 × floor(copy_input / 26) 等价于 x mod 26。循环每次除以 26,因此是在按 string_len26 做进制转换。

x - 26 * floor(x / 26) = x mod 26

字节进入链表

每个字符先 XOR 7,再由 sub_1400011E0 分配 16 字节节点,写入字符与 next 指针。第一次见到的伪代码,其实是链表追加。

node = malloc(0x10) · node.value = char ^ 7 · node.next = NULL

补上缺失的 /

目标串看似只有 13 个字符,但比较从索引 -1 开始。根据 Writeup 在前面补 /,得到 14 字节,再反向做 XOR 与 26 进制还原。

s1 = "/..v4p$$!>Y59-" · result = result * 26 + s2.index(chr(ord(c)^7))

Complete article

完整原文

文字、链接、图片、表格与代码均保留 开始阅读

题目

题目链接

CTF Show 20

解题

main函数

C
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  qword_140004618 = (__int64)malloc(0x10ui64);
  qword_140004620 = qword_140004618;
  *(_QWORD *)(qword_140004618 + 8) = 0i64;
  sub_140001020("请输入正确的数字:\n");
  sub_140001080("%lld");
  ((void (__fastcall __noreturn *)())sub_1400010E0)();
}

观察发现是传入flag,然后程序检验flag是否正确

sub_1400010E0进行了一系列运算,我基于猜测对变量进行简单重命名和分析

C
void __fastcall __noreturn sub_1400010E0(char *input, __int64 a2)
{
  int length; // r9d
  __int64 copy_input; // r8
  char *addr_index; // r10
  char content; // al
  __int64 copy_length; // rbx
  char v7; // cl
  char v8; // [rsp+1Fh] [rbp-3F9h]
  char zero; // [rsp+20h] [rbp-3F8h] BYREF

  length = 0;
  copy_input = (__int64)input;
  if ( input )
  {
    addr_index = &zero;
    do
    {
      ++addr_index; //这里很明显是给一片连续内存写入经过处理后的string_len26
      ++length; //记录长度
      content = string_len26[copy_input + -26 * (copy_input / 26)];
      copy_input /= 26i64;
      *(addr_index - 1) = content; //这里很明显是给一片连续内存写入经过处理后的string_len26
    }
    while ( copy_input );
  }
  copy_length = length;
  while ( copy_length )
  {
    v7 = *(&v8 + copy_length--);
    sub_1400011E0(v7 ^ 7);
  }
  sub_140001220();
}

C
do
{
    ++addr_index;
    ...
    *(addr_index - 1) = content; 
}
while ( copy_input );

这里很明显是给一片连续内存写入经过处理后的 string_len26


C
do
{
    content = string_len26[copy_input + -26 * (copy_input / 26)];
    copy_input /= 26i64;
}
while ( copy_input );

这里我看了很久,整理一下逻辑就是,copy_input每次会除以26,然后取出 content

至于 string_len26[copy_input + -26 * (copy_input / 26)]

展开有 copy_input - copy_input这显然不对,因为 copy_input / 26是向下取整的

实际上这个表达式等价于:

xmod26x \bmod 26

这是因为:

x26×x26=xmod26x - 26 \times \lfloor \frac{x}{26} \rfloor = x \bmod 26

所以这就是个26 进制转换,表是 string_len26

我不知道为什么 LaTeX\LaTeX 会以不同的形式渲染两遍,可能是 MDX 的渲染机制导致的,这可能导致这部分看起来很奇怪。

如果有大佬知道为什么,请在Issue中告诉我,谢谢。


C
_QWORD *__fastcall sub_1400011E0(char a1)
{
  _QWORD *addr_result; // rax
  __int64 v3; // rdx

  addr_result = malloc(0x10ui64); // 分配内存 16字节,可存储2个QWORD(每个QWORD为8字节,即64位)
  v3 = qword_140004618; // 获取链表头
  qword_140004618 = (__int64)addr_result; // 更新链表头
  *(_QWORD *)(v3 + 8) = addr_result; // 将新节点添加到链表头部
  *(_BYTE *)v3 = a1; // 设置新节点的值
  addr_result[1] = 0i64; // 设置新节点的下一个节点为NULL
  return addr_result;
}

第一次接触逆向的伪代码,这部分看得也十分吃力,丢给gpt了

告诉我是个 链表,我一看,确实。

不过这部分似乎不重要。


C
void __noreturn sub_140001220()
{
  __int64 v0; // r9
  int v1; // ecx
  __int64 v2; // rdx
  char v3; // al
  int v4; // r8d
  __int64 v5; // r9
  char v6; // cl
  int v7; // eax

  v0 = qword_140004620;
  v1 = 0;
  v2 = 0i64;
  while ( 1 )
  {
    v3 = *(_BYTE *)v0;
    v4 = v1 + 1;
    v5 = *(_QWORD *)(v0 + 8);
    if ( v3 != aV4pY59[v2 - 1] )
      v4 = v1;
    qword_140004620 = v5;
    if ( !v5 )
      break;
    v6 = *(_BYTE *)v5;
    v7 = v4 + 1;
    v0 = *(_QWORD *)(v5 + 8);
    if ( v6 != aV4pY59[v2] )
      v7 = v4;
    qword_140004620 = v0;
    if ( v0 )
    {
      v2 += 2i64;
      v1 = v7;
      if ( v2 < 14 )
        continue;
    }
    goto LABEL_11;
  }
  v7 = v4;
LABEL_11:
  if ( v7 == 14 )
    sub_1400012E0();
  sub_1400012B0();
}

这里发现 aV4pY59..v4p$$!>Y59-,两位两位取的,判断匹配数是否为14

1744385604640

不过发现这只有13个,少了一个,看代码发现索引从-1开始

1744385667471

这里也是参考了Writeup,前面要补一个 /

代码

PYTHON
s1 = "/..v4p$$!>Y59-"
s2 = ")(*&^%489$!057@#><:2163qwe"

xor7 = [chr(ord(i) ^ 7) for i in s1]
# xor7.reverse()
result = 0
for i in xor7:
    num = s2.index(i)
    result *= 26
    result += num
print(result)

总结

写完blog之后感觉这题也就这样吧,可能还是我太菜了。

.text:0000000140001000

Reverse 做题记录 (1)

MnZn第一次做RE题

tech CTFReverse

CTF Show - 逆向4

IDA Pseudocode .text:140001000
1int __fastcall __noreturn main(int argc, const char **argv, const char **envp) 2{ 3 qword_140004618 = (__int64)malloc(0x10ui64); 4 qword_140004620 = qword_140004618; 5 *(_QWORD *)(qword_140004618 + 8) = 0i64; 6 sub_140001020("请输入正确的数字:\n"); 7 sub_140001080("%lld"); 8 ((void (__fastcall __noreturn *)())sub_1400010E0)(); 9}

Program receives input and passes it to sub_1400010E0 for validation.

Decompiled .text:1400010E0
1void __fastcall __noreturn sub_1400010E0(char *input, __int64 a2) 2{ 3 int length; // r9d 4 __int64 copy_input; // r8 5 char *addr_index; // r10 6 char content; // al 7 __int64 copy_length; // rbx 8 9 length = 0; 10 copy_input = (__int64)input; 11 if ( input ) 12 { 13 addr_index = &zero; 14 do 15 { 16 ++addr_index; 17 ++length; 18 content = string_len26[copy_input + -26 * (copy_input / 26)]; 19 copy_input /= 26i64; 20 *(addr_index - 1) = content; 21 } 22 while ( copy_input ); 23 } 24 copy_length = length; 25 while ( copy_length ) 26 { 27 v7 = *(&v8 + copy_length--); 28 sub_1400011E0(v7 ^ 7); 29 } 30 sub_140001220(); 31}
Key Insight
string_len26[copy_input + -26 * (copy_input / 26)] is equivalent to:
x mod 26 = x - 26 × x / 26

This is a base-26 conversion. Each digit maps through string_len26 lookup table, then XOR'd with 7 before being stored into a linked list.

Decompiled .text:1400011E0
1_QWORD *__fastcall sub_1400011E0(char a1) 2{ 3 _QWORD *addr_result; // rax 4 __int64 v3; // rdx 5 6 addr_result = malloc(0x10ui64); 7 v3 = qword_140004618; 8 qword_140004618 = (__int64)addr_result; 9 *(_QWORD *)(v3 + 8) = addr_result; 10 *(_BYTE *)v3 = a1; 11 addr_result[1] = 0i64; 12 return addr_result; 13}

Allocates 16 bytes (2 QWORDs) per node. Standard singly linked list insert at head.

Decompiled .text:140001220
1void __noreturn sub_140001220() 2{ 3 __int64 v0; // r9 4 int v1; // ecx 5 __int64 v2; // rdx 6 char v3; // al 7 8 v0 = qword_140004620; 9 v1 = 0; 10 v2 = 0i64; 11 while ( 1 ) 12 { 13 v3 = *(_BYTE *)v0; 14 v4 = v1 + 1; 15 v5 = *(_QWORD *)(v0 + 8); 16 if ( v3 != aV4pY59[v2 - 1] ) 17 v4 = v1; 18 qword_140004620 = v5; 19 if ( !v5 ) break; 20 ... 21 if ( v2 < 14 ) continue; 22 goto LABEL_11; 23 } 24 LABEL_11: 25 if ( v7 == 14 ) 26 sub_1400012E0(); // success 27 sub_1400012B0(); // fail 28}
Finding
Target string aV4pY59 = "..v4p$$!>Y59-" (13 chars). Index starts at -1, so prefix with / to get 14 chars total.
solve.py Python 3
1s1 = "/..v4p$$!>Y59-" 2s2 = ")(*&^%489$!057@#><:2163qwe" 3 4xor7 = [chr(ord(i) ^ 7) for i in s1] 5result = 0 6for i in xor7: 7 num = s2.index(i) 8 result *= 26 9 result += num 10print(result)

Base-26 conversion + XOR(7) + linked list storage + string comparison.